Combining traditional configuration management with Docker

by on

Configuration management systems like Ansible, Chef, Salt and Puppet are the traditional and proven way to automate server management. Using a tool like one of these has a lot of benefits over manual configuration: Setting up a new server (for new projects or to replace a broken server) can be performed faster and more accurately than we could do manually. It also makes sure that servers and individual services are configured exactly the same when you’re doing it multiple times. No doubt about the benefits of configuration management systems.

Hello Docker!

Then Docker came and brought a new way to manage services. With the Dockerfile it introduced a standardized way to describe exactly what should be running inside a container. While containers were nothing new, the easy of use of the Docker toolchain changed everything.

If you’re just managing a few servers and are not planning to scale using Kubernetes or similar orchestration tools, using docker can make managing your services harder. Keeping containers up to date means you have to manage another package manager and configuration depends on whatever the image maintainer decided to add to the image. Having a bit more flexibility in the software you’re running is awesome, though! That’s why I decided to combine services running inside Docker containers with traditional configuration management.

Hybrid configuration

Since I want to use my existing Ansible code to manage nginx and it’s configuration, I need to be able to:

But I also want features that I can’t get on my standard debian system like TLS 1.3 and brotli compression.

Managing a containerized nginx service with systemd

First step was to create an nginx docker image with support for everything I want. The systemd unit below takes care of downloading and starting the container on my server. Volumes are mounted on every important path listed above to make sure I can configure everything as if the nginx service was installed using the default Debian package.

[Unit]
Description=Nginx
After=docker.service
Requires=docker.service

[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=/usr/bin/docker pull sorcix/nginx:latest
ExecStart=/usr/bin/docker run --net=host --name nginx --rm -v /etc/nginx:/etc/nginx:ro -v /var/log/nginx:/var/log/nginx:rw -v /etc/ssl:/etc/ssl:ro -v /var/www:/var/www sorcix/nginx:latest
ExecReload=/usr/bin/docker exec nginx nginx -s reload
ExecStop=/usr/bin/docker exec nginx nginx -s quit

[Install]
WantedBy=multi-user.target

Networking & security

The --net=host part makes the container use host networking instead of the separate docker network that is usually created for containers. Forwarding ports 80 and 443 is not required (which actually makes it handle traffic a little faster) and we can connect to services running on the host. Note that this also means that the additional security that containers usually provide is now missing. That’s fine with me as I’m using this as a drop in replacement for the default Debian nginx package which does not provide containerization at all.

Updates

The ExecStartPre command downloads new versions of the sorcix/nginx container every time I restart nginx. As I manage the container image myself this isn’t really an issue, but you should disable this when using images created by others.