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.comshould hit a blogging container,
app.example.comshould 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
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
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
ghost: image: ghost environment: VIRTUAL_HOST: robertcunningham.xyz VIRTUAL_PORT: 2368 url: https://robertcunningham.xyz expose: - 2368 volumes: - ./ghost:/var/lib/ghost/content
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.
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'
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
environment: LETSENCRYPT_HOST: robertcunningham.xyz LETSENCRYPT_EMAIL: [email protected]
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
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.
 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.