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.