Docker container as a Linux system service
Introduction
There are many different ways to orchestrate docker container management, initialization, deployment, etc. Even Docker brings its own vanilla tool and mode. There are also many other third party container orchestration tools such as Kubernetes, Rancher, Apache Mesos, etc.
Docker daemon offers simple means to start, stop, manage and query status of deployed containers. In this post we’ll cover how to use a docker + systemd only approach to deploy containers as Linux services without the need for third party tools or complex deployment descriptors.
In this tutorial we will show how to deploy Portainer as a Linux systemd service.
Existing container
The simplest method to deploy a container as a service is to create a docker container with a given name and then map each of the docker operations (start
and stop
) to systemd service commands.
1docker run -d --name portainer --privileged -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v /opt/docker/data/portainer:/data portainer/portainer
Once we’ve created this container we can start, stop and restart the container using the regular docker commands by indicating the container name (docker stop portainer, docker start portainer, docker restart portainer).
We can create a new systemd unit file with the service description by creating a new file in /etc/systemd/system/
. For the sake of this example we’ll create a new portainer.service
file with the following contents.
1[Unit]
2Description=Portainer container
3After=docker.service
4Wants=network-online.target docker.socket
5Requires=docker.socket
6
7[Service]
8Restart=always
9ExecStart=/usr/bin/docker start -a portainer
10ExecStop=/usr/bin/docker stop -t 10 portainer
11
12[Install]
13WantedBy=multi-user.target
The unit file creates a new service and maps docker start
and docker stop
commands to the service start and stop sequences.
The unit file describes as dependencies the network-online target and the docker socket, if docker socket doesn't start this service won’t either. Also adds a dependency to docker.service, so this service won’t run until docker.service
has started.
We now can start / stop the service by issuing the corresponding command:
1systemctl start portainer
2systemctl stop portainer
We can also install the service to run at start up by running:
1systemctl enable portainer.service
Create container if applicable on start
We can improve the previous unit file to create the container if it doesn’t exist, this will allow us to skip the first step of manually creating the container (docker run…
) and can be used to deploy containers by simply creating the service descriptor unit file.
1[Unit]
2Description=Portainer container
3After=docker.service
4Wants=network-online.target docker.socket
5Requires=docker.socket
6
7[Service]
8Restart=always
9ExecStartPre=/bin/bash -c "/usr/bin/docker container inspect portainer 2> /dev/null || /usr/bin/docker run -d --name portainer --privileged -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v /opt/docker/data/portainer:/data portainer/portainer"
10ExecStart=/usr/bin/docker start -a portainer
11ExecStop=/usr/bin/docker stop -t 10 portainer
12
13[Install]
14WantedBy=multi-user.target
We simply added an additional line with an ExecStartPre
instruction. Systemd unit files have certain limitations when executing commands in the ExecStart, ExtecStop, ExecStartPre…. definitions. In order to overcome these limitations we encapsulate the command inside a /bin/bash -c
command that allows us a little more flexibility.
The first command (docker container inspect
) will check to see if a container with this name already exists in the docker host machine. If this command throws an error (i.e. container doesn't exist) the next instruction will be run. This command is the same as we used previously to create the container manually.
It is really important to execute the docker run
command with the -d
(detached) flag, not doing so will prevent the command to return thus not allowing the next step (ExecStart) to run, so the systemd will never become active.
Depending on the previous existence of the container the service start sequence will behave one way or another:
- A container named portainer already exists:
- ExecStartPre will return with no error on the first command (
docker container inspect…
) thus won’t execute the latter (docker run…
). - ExecStart will run normally and the process will attach to the container.
- ExecStartPre will return with no error on the first command (
- A container named portainer doesn’t exist:
- ExecStartPre will return with an error on the first command (
docker container inspect…
), the second command will create and start the container in detached mode returning the control to systemctl. - ExecStart will run (
docker start
) which will not have any effect but will attach to the running container.
- ExecStartPre will return with an error on the first command (
Conclusion
In this tutorial we’ve shown a simple way to deploy docker containers as Linux systemd services. This is a “cheap” alternative to Kubernetes or any other container orchestration tool, although it’s not as reliable and should only be used in very specific cases.
Comments in "Docker container as a Linux system service"
I am just setting up a simple DNS server for a home lab and I wanted to do it with CoreDNS in a docker container. This was a great help.
Having a little trouble getting the container to stop but I will try to figure that out. I am also going to work on having a template file so I can set up multiple instances as detailed at https://coreos.com/os/docs/latest/using-environment-variables-in-systemd-units.html.
Cheers again,
Ed.