Docker is neat. I don't believe it's the ultimate solution for all our software packaging and delivery problems, but still, as a way to easily run something someone else has made, it's neat. So when I decided to migrate my blog to Ghost, I though I might use the docker image they provide to deploy it on my server.
However, there is one thing that bothers me with docker: its configuration is very imperative. You type commands to set up your containers the way you want them, which is quite straightforward, but has a few inconveniences:
- At the end, the only way to know what you have deployed is to look at your shell history (discarding the commands that were invalid, misspelled, or canceled). It is pretty impossible to reproduce the same setup one month later if you have not precisely documented your first run.
- Upgrading is tedious. In an imperative context, upgrading means undoing some of the setup work (but not necessarily all), downloading new stuff, then re-doing the setup work (but not necessarily the exact same). Setup and upgrade are two different operations that are related but not quite the same.
- Cleaning up (removing a container) is yet another operation with yet another set of interactions with the other operations.
In short: it's annoying to use, annoying to automate, and annoying to come back to.
I prefer nice declarative configurations. In a declarative configuration, you specify what final state you want for the system, and the tool does what's needed to put the system in that state. Setup, upgrade, and cleanup procedure are just implementations details. Upgrading a system is just a matter of saying "this parts must be at that version now".
Strangely, I've had a hard time finding the declarative configuration on top of docker I was looking for. Kubernetes seemed overkill, fig seemed dead. I finally realized that docker-compose, which is part of the docker toolbox, was exactly what I was looking for. It has a nice container setup description format in yaml, which let you do all you can do with the docker command line, but in a nice, commentable, version-controllable, file.
For a basic ghost setup it looks like this:
compiletoi: image: ghost volumes: - /var/lib/hosting/compiletoi:/var/lib/ghost environment: VIRTUAL_HOST: compiletoi.net NODE_ENV: production nginx-proxy: image: jwilder/nginx-proxy ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/tmp/docker.sock:ro
It's a ghost blog with a nginx reverse proxy in front of it. Running it is just
docker-compose up --detach, and upgrading it is just a matter of running
docker-compose pull to get the new images, and re-running
docker-compose up --detach to re-create the containers. And that's it.
That's why I like declarative interfaces: simple, clean, and all the heavy-lifting is abstracted away, because once you have said what you want, the "getting it" part can be automated.
As a bonus, here is the full (slightly obfuscated) version of my
docker-compose.yml file. It contains two blogs served by a single nginx reverse proxy (thanks to the amazing nginx-proxy) in https, with certificates thanks to letencrypt and letsencrypt-nginx-proxy-companion.
# First of all, the blogs compiletoi: image: ghost volumes: - /var/lib/hosting/compiletoi:/var/lib/ghost environment: VIRTUAL_HOST: compiletoi.net LETSENCRYPT_HOST: compiletoi.net LETSENCRYPT_EMAIL: email@example.com NODE_ENV: production restart: always otherblog: image: ghost volumes: - /var/lib/hosting/otherblog:/var/lib/ghost environment: VIRTUAL_HOST: blog.example.com LETSENCRYPT_HOST: blog.example.com LETSENCRYPT_EMAIL: firstname.lastname@example.org NODE_ENV: production restart: always # Then, the nginx in front of them nginx-proxy: image: jwilder/nginx-proxy ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - /etc/nginx/certs - /var/lib/hosting/vhost.d:/etc/nginx/vhost.d - /usr/share/nginx/html restart: always # Finally, letsencrypt for the certificates letsencrypt: image: letsencrypt-nginx-proxy-companion volumes: - /var/run/docker.sock:/var/run/docker.sock:ro volumes_from: - nginx-proxy restart: always