Earlier on I've shown you how I run docker containers on NAS using systemd. This time, I'll show you an easy way to configure systemd with Ansible that will get you running containers in no time. (and in reproducible and automated way)

Note while this configuration is very simple and even Ansible beginner should be able to follow it, I'm not going to explain how to install ansible or how to create your inventory - this might be very specific to your setup and there are good tutorials out there on that topic.

What we need here is to create service file for each container we'd like to run and make sure that service is set to start at boot time in correct order. (after docker)

I really like how well systemd works with Docker. It is still not ideal, because Docker kind of escapes the cgroups defined in systemd - something like rkt would probably integrate much better, nonetheless it's still very robust configuration and good improvement even from quite decent upstart configuration I had before.

First create role

We know, that there will be multiple containers deployed on a single machine, so to save us some repetition, we are going to create a role. For all we need, we are fine with just creating a directory structure, but if you want to get a quick and nice skeleton for your role, use ansible-galaxy like this:

ansible-galaxy init --offline -p roles/ <role_name>

I'm going to call mine docker-systemd. Once we have that done, we can start by..

Creating the template Let's take a sample template for my Plex server: [Unit] Description = Plex in Docker After = docker.service Requires = docker.service [Service] TimeoutStartSec = 0 ExecStartpre = -/usr/bin/docker stop plex ExecStartpre = -/usr/bin/docker rm plex ExecStartpre = -/usr/bin/docker pull my_registry:5000/plex ExecStart = /usr/bin/docker run --rm -t \ -v /data/media/:/media/ \ -v /data/system/plex/:/config/ \ --net host \ --name plex my_registry:5000/plex ExecStop = -/usr/bin/docker stop -t 3 plex ExecStop = -/usr/bin/docker rm plex Restart = always RestartSec = 10 [Install] WantedBy = multi-user.target It's quite straightforward stuff so far. On start we make sure that any previously launched instance is stopped and removed. This should be only needed if something weird happened (like unexpected reboot) so I'm prepending the commands with dash to ignore any failures on these steps, most of the time there will be nothing to stop. As the last pre-start step, we try to fetch the latest image, again ignoring any failures. (we still want our services to start even if repository is unreachable) The actual start is your usual docker run, but this time we're also starting it with extra flags: --rm -t to remove container on stop (I don't want persistent containers, my persistent data resides on mounted volumes) and to attach to container tty. The later gives us two things: container logs are accessible via journalctl

the docker binary will be running as long as container is running, so in case the container stops, systemd will know that it needs to restart the service to bring it back up. Stop is just some cleanup - again unnecessary under normal circumstances, so we ignore any failures on these. Systemd is also instructed to restart the service if it fails (after 10 seconds) and it has docker set as dependency, so it will start in proper order during boot. Now let's make a template out of the service file by replacing the service specific stuff with Ansible variables: [Unit] Description= {{ container_description }} After=docker.service Requires=docker.service [Service] TimeoutStartSec=0 Restart=always ExecStartPre=-/usr/bin/docker stop {{ container_name }} ExecStartPre=-/usr/bin/docker rm {{ container_name }} {% if container_pull %} ExecStartPre=-/usr/bin/docker pull {{ container_image }} {% endif %} ExecStart=/usr/bin/docker run --rm -t \ {% for port in container_ports %} -p {{ port }} \ {% endfor %} {% for volume in container_volumes %} -v {{ volume }} \ {% endfor %} {% for option in container_extra_options %} {{ option }} \ {% endfor %} --name {{ container_name }} {{ container_image }} {{ container_cmd }} ExecStop=-/usr/bin/docker stop -t 3 {{ container_name }} ExecStop=-/usr/bin/docker rm {{ container_name }} Restart=always RestartSec=10s [Install] WantedBy=multi-user.target Quite simple, isn't it? The parts that aren't absolutely trivial are: Optional docker pull - if set to true Ansible will add a startup step to first pull the latest image from registry.

Ports, Volumes and extra run options - Ansible will iterate over any defined volumes, port mappings and extra options and will include them - one line at a time Let's save the template as templates/systemd-container.service.j2 in our role directory.

Creating the tasks Let's have a look at the tasks we need to define in Ansible: --- - name : create service file template : dest : "/etc/systemd/system/{{ container_service_name }}" src : "templates/systemd-container.service.j2" register : service_config - name : reload daemon shell : systemctl daemon-reload when : service_config.changed - name : restart service service : name : "{{ container_service_name }}" state : restarted when : service_config.changed - name : enable service service : name : "{{ container_service_name }}" state : started enabled : yes There are just four steps needed: Create the service file using our template

Reload daemon configuration if the file was just created or updated

Restart service if it was created/changed

Make sure the service is started and enabled That was easy one, wasn't it? Let's save it as tasks/main.yml .