As it stands, this server has two services running: a blog and git repository. They're both routed to by an nginx proxy, and my free SSL certificates are provided by the oh-so-wonderful Let's Encrypt.
The best part is that all of that is defined in and deployed by using a single Docker Compose file.
That means the only thing I had to do on my server was install Docker (which is made easy by Docker Machine). The rest happens inside containers that play well with each other thanks to Docker Compose.
I'm going to explain the file section by section, but first, here's all of it:
version: '2' services: nginx-proxy: image: jwilder/nginx-proxy:0.4.0 container_name: nginx-proxy ports: - "80:80" - "443:443" volumes: - certs:/etc/nginx/certs:ro - /etc/nginx/conf.d - /etc/nginx/vhost.d - /usr/share/nginx/html - /var/run/docker.sock:/tmp/docker.sock:ro environment: - DEFAULT_HOST=ptrvldz.me letsencrypt-nginx-proxy: image: jrcs/letsencrypt-nginx-proxy-companion container_name: letsencrypt-nginx-proxy volumes_from: - nginx-proxy volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - certs:/etc/nginx/certs:rw blog: image: ghost:0.11.2 container_name: blog volumes: - ghost:/var/lib/ghost environment: - VIRTUAL_HOST=ptrvldz.me - LETSENCRYPT_HOST=ptrvldz.me - LETSENCRYPT_EMAILemail@example.com git: image: gogs/gogs:0.9.97 container_name: git volumes: - gogs:/data environment: - VIRTUAL_HOST=git.ptrvldz.me - VIRTUAL_PORT=3000 - LETSENCRYPT_HOST=git.ptrvldz.me - LETSENCRYPT_EMAILfirstname.lastname@example.org volumes: ghost: external: false gogs: external: false certs: external: false
The first line defines which Docker Compose file format we're going to write. The recommended format is the newer format, so we start the file with:
Next we get to the meat of the file, where we define our services. We begin our service definitions with the line:
And away we go.
Service: nginx proxy
The nginx-proxy image is one of the most magical of them all.
Let's consider a usual desired setup: you want to host various web apps on different subdomains on a single server. The way we typically solve this is by running a web server that forwards requests to the desired applications.
There is usually a bunch of boilerplate involved in getting this going, but in essence, you're just mapping a domain to some local port where your app is running.
The nginx-proxy image removes the boilerplate. Once running, it finds any containers that have the
VIRTUAL_HOST environment variable, and then forwards any requests bound for the domain defined by the variable to that container.
In other words, if we start a WordPress container with the environment variable
VIRTUAL_HOST set to
wordpress.somesite.com, then the nginx proxy will forward all requests for
wordpress.somesite.com to that WordPress container.
So let's look at the definition of the nginx-proxy:
nginx-proxy: image: jwilder/nginx-proxy:0.4.0 container_name: nginx-proxy ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - certs:/etc/nginx/certs:ro - /etc/nginx/conf.d - /etc/nginx/vhost.d - /usr/share/nginx/html environment: - DEFAULT_HOST=ptrvldz.me
The first line is the name of our service. We then define which image we want to use and what we want to call the container when it's running.
Because this is a web server that will be SSL-enabled, we specify that we want our actual "hardware" server's port 80 and 443 to be forwarded to our nginx proxy.
We then define our various volumes. First, we give the container access to our host Docker socket because that's how it will gather data about other containers. We then use a named volume (identifiable by the slash-less string before the colon) for the SSL certificates because that's where our letsencrypt container will place them. And then the remaining three volumes are defined so that our letsencrypt container can write to the files in there.
Finally, we define the environment variable
DEFAULT_HOST so the proxy knows which domain is default, in case a request does not ask for a specific domain.
Service: SSL certificate generation
If you've not yet heard of it, Let's Encrypt is a great project that allows you to get free SSL certificates. With the easy-to-use clients that exist, it's all pretty much automatic.
And if you're using nginx-proxy, it's even easier. All we give our letsencrypt container is:
- access to our nginx-proxy volumes
- the Docker socket so it can see which services need a certificate
- a place to put the certificates
Let's look at the service definition:
letsencrypt-nginx-proxy: image: jrcs/letsencrypt-nginx-proxy-companion container_name: letsencrypt-nginx-proxy volumes_from: - nginx-proxy volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - certs:/etc/nginx/certs:rw
The first three lines again define the name of the Docker Compose service, the image we're using, and the name of the running container.
We then specify that we want it to have access to the volumes that we created for our nginx-proxy.
And finally, we give it access to the Docker socket and tell it to use a named volume for the certs. Note that the named volume here (
certs) is the same as the named volume being accessed by the nginx-proxy.
We finally get to an actual application, a blog.
I knew I didn't want to use something heavy like Wordpress. And I love static-site generators, but I have to spend some time thinking about a setup.
So I went with a compromise: Ghost. I can self-host it, and posts are written in Markdown, so I can port to some static-site generation setup with ease later.
So now that all our preparation was done in the previous service definitions, let's see how we can define our Ghost blog:
blog: image: ghost:0.11.2 container_name: blog volumes: - ghost:/var/lib/ghost environment: - VIRTUAL_HOST=ptrvldz.me - LETSENCRYPT_HOST=ptrvldz.me - LETSENCRYPT_EMAILemail@example.com
Again, the first three lines are the Docker Compose service name, the image that we'll be using, and container name.
We then define a named volume for our data because we want our blog data to exist even if we recreate this container.
And now the magic.
- By setting
ptrvldz.me, our nginx-proxy will know to forward requests for
ptrvldz.meto this container.
- By setting
LETSENCRYPT_EMAIL, our letsencrypt container will use that data to create SSL certificates for this service.
Service: Git repository
And so our final service, the Git repository. I went with Gogs. I originally liked Gogs because of it's portability, thanks to it being a single binary. Considering I'm running it inside a container, that doesn't matter as much, but oh well.
Let's take a look at the service definition:
git: image: gogs/gogs:0.9.97 container_name: git volumes: - gogs:/data environment: - VIRTUAL_HOST=git.ptrvldz.me - VIRTUAL_PORT=3000 - LETSENCRYPT_HOST=git.ptrvldz.me - LETSENCRYPT_EMAILfirstname.lastname@example.org
It's all the same as the blog, but there is one difference:
If a container only exposes a single port, then our nginx-proxy is smart enough to know it should forward requests to that port. However, if several ports are exposed by a container, you can specify which one is correct by setting
VIRTUAL_PORT to the correct one.
The last section of the file is the volumes block. If you use named volumes, you must define them in this section, and so we do:
volumes: ghost: external: false gogs: external: false certs: external: false
false for our volumes will tell Docker Compose that it should create them if they're not there.
And we're done
So to reiterate, we have nginx forwarding requests to two applications, each with valid SSL certificates. With each distinct service in its own container. All this accomplished with 57 lines of configuration in a single file.
And what if we wanted to add one more service, like that WordPress instance we mentioned earlier? Probably about 10 more lines and we'd have it routed to by nginx and secured with its own certificate.
I thank the powers that be for containers.