Standard Docker-Compose Deploys
I've been deploying a bunch of web apps recently, and it turns out there's one pattern that's so common it might even be called classic. Basically:
- Each subdomain should get routed to its own docker container.
blog.example.com
should hit a blogging container,app.example.com
should hit a React app, etc. - Caching, compression, and SSL (issuance and renewal) should be automatically handled.
This tutorial explains how to do all of that with a single docker-compose
file.
This cornerstone of this setup is a docker image called nginx-proxy
. Each time a request hits your server, this container checks the subdomain and chooses which docker container will handle it. It also handles caching, compression, and SSL (if the certificates are available).
To set it up, add the following to a docker-compose.yml
file.
version: '2'
services:
nginx-proxy:
image: nginxproxy/nginx-proxy
ports:
- '80:80'
- '443:443'
volumes:
- /etc/nginx/certs:/etc/nginx/certs
- /etc/nginx/vhost.d:/etc/nginx/vhost.d
- /usr/share/nginx/html:/usr/share/nginx/html
- /var/run/docker.sock:/tmp/docker.sock:ro
This file says that the service 'nginx-proxy' will be based on the image nginxproxy/nginx-proxy
, will communicate with the outside world on ports 80 and 443, and will be able to access the locations listed under volumes
on your host system.
To run it, install docker and docker-compose, then run docker-compose up
. If you navigate to localhost
, you should see a black-on-white nginx error. That's because there are no other containers for nginx-proxy
to forward your request to.
Let's add one. It will be an instance of Ghost, the popular blogging platform. Add this to the end of your docker-compose.yml
, under the services
heading.
ghost:
image: ghost
environment:
VIRTUAL_HOST: robertcunningham.xyz
VIRTUAL_PORT: 2368
url: https://robertcunningham.xyz
expose:
- 2368
volumes:
- ./ghost:/var/lib/ghost/content
The VIRTUAL_HOST
and VIRTUAL_PORT
variables tell nginx-proxy to forward requests for the root domain (robertcunningham.xyz
) to port 2368 of the ghost container. The url
variable tells Ghost where it'll live, and the volumes
heading provides a place on the host system where Ghost can save its files.
Re-run docker-compose up
, and navigate to your website (robertcunningham.xyz
, in our case). Behind the scenes, nginx-proxy
will delegate this request to the newly-created ghost container, and you should see a brand new installation of Ghost.
You can continue to do this for your other services. For example, to host your React app at app.robertcunningham.xyz
, you could add something like this:
robertsapp:
image: 43459873.dkr.ecr.us-east-1.amazonaws.com/robertsapp
expose:
- 8080
environment:
VIRTUAL_HOST: app.robertcunningham.xyz
VIRTUAL_PORT: 8080
This will download the docker image robertsapp
from Amazon ECR, and forward any requests for app.robertcunningham.xyz
to it.
Now we'll configure SSL. SSL is handled by a container called nginxproxy/acme-companion
. Its job is to check whether the SSL certificates on your disk exist and are up to date. If they're not, it issues or renews them. Then nginx-proxy
can use those certificates when it accepts incoming connections.
For this to work, add the following to the bottom of your docker-compose file.
nginx-proxy-ssl:
image: nginxproxy/acme-companion
volumes:
- /etc/acme.sh:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
volumes_from:
- nginx-proxy
environment:
#ACME_CA_URI: 'https://acme-staging-v02.api.letsencrypt.org/directory'
NGINX_PROXY_CONTAINER: 'nginx-proxy'
The volumes_from
statement allows the nginx-proxy-ssl
container to put the certificates in a place where nginx-proxy
can find them. If you uncomment the ACME_CA_URI
environment variable, you'll get your certificates from the LetsEncrypt staging environment. The staging environment is more forgiving with rate limits, and once everything works, you can comment out this line to get real certificates.
You'll also need to add the following to your ghost
container.
environment:
LETSENCRYPT_HOST: robertcunningham.xyz
LETSENCRYPT_EMAIL: your_email@gmail.com
And that's it! You're done. You can run docker-compose up -d
, which will run the process in the background, so it doesn't get terminated when you exit your SSH session. If you want to stop it in the future, you can run docker-compose down
.
That's it! Happy deploying.
If you're still unsure of anything, you can find the full docker-compose behind this website here for reference.
Notes
[1] This tutorial relies heavily upon docker. I'll make a quick plug for it here. Of all the developer tools I know, docker shares the title with git for most useful. If you don't know it, spend two hours today to learn it. It'll save you time for the rest of your life.