James Stanley

Zero-downtime Docker container deployments with nginx

Thu 14 May 2020

Tagged: software

Docker doesn't let you reassign port mappings while containers are running. That means if you want to deploy a new version of your application, you need to stop the old one and then start the new one, and there is a brief period of downtime in between. I wrote Ngindock to solve this problem.

I recently got sold on Docker and am in the process of Dockerising many of the services I run. Currently most of the stuff I've written uses Mojolicious and runs under hypnotoad, behind nginx as a reverse proxy. To hot-deploy a new version of the application, I can just:

$ hypnotoad app

And hypnotoad will replace itself with a version running the new code, with no downtime.

This doesn't work very well in the Docker case, because the Docker images are meant to be immutable and therefore the version of the code that hypnotoad has access to is also immutable. There are various solutions for downtime-free deployments for people who are a bit more Cloud Native than me, including Traefik, Kubernetes, and Docker Swarm.

I wanted something that would be minimally disruptive to all of my existing non-Dockered services. The solution I had in mind was:

  1. start a new container running the new image, listening on a different port
  2. rewrite the nginx config to direct traffic to the new port
  3. reload nginx to make it use the new config
  4. stop the old container

I couldn't find a tool that did this, so I made one. It seems to work OK but I would definitely recommend caution before using it on anything you care about. The README on github has relatively detailed instructions on how it's used and configured, but in brief:

  • make a file in your deployment directory called ngindock.yaml, describing the Docker image and container names, port numbers, etc.
  • run ngindock whenever you want to trigger a fresh deployment

It aims to leave your nginx.conf completely unchanged (including comments and whitespace) apart from the port number. If it fails at this, that's a bug, please open a github issue.

If you want to give it a try, clone the https://github.com/jes/ngindock repo and follow the instructions in the demo/ directory.

Example ngindock.yaml:

nginx_conf: /etc/nginx/conf.d/app.conf
nginx_upstream: app
ports: [3000,3001]
container_port: 3000
image_name: my_image
container_name: my_container
health_url: /

Example session with "-v":

$ sudo ngindock -v 
[Thu May 14 17:20:31 2020] plan to move from port 3000 to 3001
[Thu May 14 17:20:31 2020] start container listening on 3001...
[Thu May 14 17:20:31 2020] started container my_container_ngindock_new
[Thu May 14 17:20:31 2020] wait for container health check...
[Thu May 14 17:20:32 2020] update nginx to direct traffic to port 3001...
[Thu May 14 17:20:32 2020] remove old container if it exists...
[Thu May 14 17:20:32 2020] stopping container my_container
[Thu May 14 17:20:33 2020] removing container my_container
[Thu May 14 17:20:33 2020] rename my_container_ngindock_new to my_container...

If you like my blog, please consider subscribing to the RSS feed or the mailing list:

James Stanley - james@incoherency.co.uk | ricochet:it2j3z6t6ksumpzd | jesblogfnk2boep4.onion | [rss]