Ansible role for creating Systemd services for docker containers
Docker role mhutter.docker-systemd-service
Generic role for creating systemd services to manage docker containers.
Example
Example of a Systemd unit for your app "myapp" that links to an already existing container "mysql":
- name: Start WebApp
include_role:
name: mhutter.docker-systemd-service
vars:
container_name: myapp
container_image: myapp:latest
container_links: ["mysql"]
container_volumes:
- "/data/uploads:/data/uploads"
container_ports:
- "3000:3000"
container_hosts:
- "host.docker.internal:host-gateway"
container_env:
MYSQLROOTPASSWORD: "{{ mysqlrootpw }}"
container_labels:
- "traefik.enable=true"
This will create:
- A file containing the env vars (either
/etc/sysconfig/myappor/etc/default/myapp). - A systemd unit which starts and stops the container. The unit will be called
<name>_container.serviceto avoid name clashes.
Role variables
container_name(required) - name of the container
Docker container specifics
container_image(required) - Docker image the service usescontainer_args- arbitrary list of arguments to thedocker runcommand as a stringcontainercmd(default: []_) - optional list of commands to the container run command (the part after the image name)container_env- key/value pairs of ENV vars that need to be presentcontainervolumes(default: []_) - List of-vargumentscontainerhostnetwork(default: false) - Whether the host network should be usedcontainerports(default: []_) - List of-pargumentscontainerhosts(default: []_) - List of--add-hostargumentscontainerlinks(default: []_) - List of--linkargumentscontainerlabels(default: []_) - List of-largumentscontainerdockerpull(default: yes) - whether the docker image should be pulledcontainerdockerpullforcesource(default: yes) - whether the docker image pull should be executed at every time (seedockerimage.forcesource)containercapadd(default []) - List of capabilities to addcontainercapdrop(default {}) - List of capabilities to dropcontainernetwork(default "") - Network settingscontaineruser(default "") - User settingscontainerhostname(default ""_) - Container host name:--hostnameflagcontainerdevices(default []_) - List of devices to addcontainerprivileged(default false_) - Whether the container should be privilegedcontainerstartpost- Optional command to be run by systemd after the container has started
Systemd service specifics
serviceenabled(default: yes_) - whether the service should be enabledservicemasked(default: no_) - whether the service should be maskedservicestate(default: started_) - state the service should be in - set to
absent to remove the service.
servicerestart(default: yes_) - whether the service should be restarted on changesservicename(default:<containername>_container) - name of the systemd serviceservicesystemdoptions(default: []) - Extra options to include in systemd service fileservicesystemdunit_options: (default{"After": "docker.service", "PartOf": "docker.service", "Requires": "docker.service"}), key/value defining the content of the[Unit]service section.
Installation
This role requires the docker python module. Install it with pip3 install docker or apt install python3-docker (or drop the 3 for python 2.x).
Put this in your requirements.yml:
- role: mhutter.docker-systemd-service
and run ansible-galaxy install -r requirements.yml.
Gotchas
- When the unit or env file is changed, systemd gets reloaded but existing containers are NOT restarted.
- Make sure to quote values for
containerports,containerhosts,container_volumesand so on, especially if they contain colons (:). Otherwise YAML will interpret them as hashes/maps and ansible will throw up.
About orchestrating Docker containers using systemd.
The concept behind this is to define systemd units for every docker container. This has some benefits:
systemdis a well-known interface- all services are controllable via the same tool (
systemctl) - all logs are accessible via the same tool (
journalctl) - dependencies can be defined
- startup behaviour can be defined
- by correctly defining the unit (see below), we can ensure we always have a clean container.
myapp_container.service unit file (about what's produced by above code):
[Unit] # define dependencies After=docker.service PartOf=docker.service Requires=docker.service
[Service] # Load ENV vars from a file. Note that this env vars will only be # accessible in the context of the Exec* commands, and not within the # container itself. To make env-vars accessible within the Container, we use # the --env-file flag for the docker run command. EnvironmentFile=/etc/sysconfig/myapp
# Even though we explicitly run the container using the --rm flag, there # may be leftover containers (eg. after a system-, docker- or app-crash). # Starting a container with an existing name will always fail.
ExecStartPre=-/usr/bin/docker rm -f myapp
# actually run the container. # --name to identify the container # --rm ensure the container is removed after stopping # --env-file make ENV vars accessible to app # --link mysql link to a container named mysql. The DB will then be # accesible at mysql:3306 # -v mount /data/uploads into the container # -p 3000:3000 expose port 3000 on the network ExecStart=/usr/bin/docker run --name myapp --rm --env-file /etc/sysconfig/myapp --link mysql -v /data/uploads:/data/uploads -p 3000:3000 registry.cust.net/myapp/myapp:latest # note that there is no --restart parameter. This is because restarting # is taken care of by systemd.
# Stop command. ExecStop=/usr/bin/docker stop myapp
# Ensure log messages are correctly tagged in the system log. SyslogIdentifier=myapp
# Auto-Restart the container after a crash. Restart=always
[Install] # make sure service is started after docker is up WantedBy=docker.service