Automatic Start-up with systemd
Since our Docker containers are web services, they need to be running all the time in order to be useful. To ensure this happens, it's useful to automatically start them when the host machine boots. We can do this easily using systemd on Linux.
TL;DR: if you don't want to learn how this works and you just want the solution, download the attached docker-compose@.service
file and place it in the /etc/systemd/system
directory. Then skip to Using our new service.
About systemd
Systemd is a service manager and init system. This means it is responsible for starting, stopping, and supervising services running on the machine. Systemd has the capability to automatically start services on boot, as well as managing dependencies and ordering. This allows us to automatically bring our Docker Compose stacks up when our host machine boots.
Systemd also has a feature called template units.
Template units have names that end with an @
.
They allow you to define a generic service that can have specific instances.
We will use this to define a generic service for Docker Compose stacks.
Then we'll create specific instances of this for each of our different stacks.
The docker-compose@
service
Creating the file
Fist, we have to create a new unit file. We can do that in two ways: (1) use the systemctl edit
command or (2) place the file in the /etc/systemd/system
directory.
Option 1 handles this automatically, so that's what I'll use.
We'll run the following command to create a new service file. The name can be arbitrary, but it must end with @.service
to make it a template service file.
# systemctl edit --full docker-compose@.service
Defining the service
Here are the contents of the docker-compose@.service
file:
[Unit]
Description=Start Docker Compose stack at %I
Requires=docker.service
After=docker.service
[Service]
WorkingDirectory=%I
Type=oneshot
RemainAfterExit=true
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
ExecReload=/usr/bin/docker compose up -d
[Install]
WantedBy=multi-user.target
Now let's walk through what it does.
The [Unit]
section
The [Unit]
section contains metadata and dependency information.
The Description
property is just a description for the unit.
Don't worry about the %I
placeholder yet. We'll get to that later.
The Requires
property defines units that are required for the docker-compose@
service to start.
Since we'll be using Docker, we need Docker to be running.
The After
directive tells systemd to wait until docker.service
has started before starting docker-compose@.service
.
Without this, they would both start simultaneously.
The [Service]
section
The [Service]
section contains instructions for systemd to start, stop, and supervise our service.
Systemd services can be started, stopped, and reloaded. Reloading means they'll reload their configuration.
Services can also be restarted, but this is implemented as just stopping and starting the service sequentially, so it isn't really a separate action as far as we're concerned.
The ExecStart
, ExecStop
, and ExecReload
directives define commands to run for each action.
We just call docker compose up/down
for each one.
We pass the -d
flag when bringing the stack up as we don't need systemd to capture the log output since Docker already handles that.
Since Docker Compose automatically reloads the docker-compose.yml
file when docker compose up
is run, the reload and start actions can be the same.
The WorkingDirectory
option is the most important one here. It tells systemd to change to the given directory before starting the service. But %I
isn't a directory. What's up with that?
The %I
placeholder
Remember how I mentioned template units? When activating a template unit, you have to specify a value after the @
sign, like docker-compose@/srv/swag.service
. Systemd will take the value after the @
sign, called the instance name, and fill it in wherever %I
occurs in the unit file.
Systemd requires the instance name to be escaped. This can be done manually or by using the systemd-escape
command. For simple paths like the ones I'm using, slashes (/
) get replaced with hyphens (-
), meaning we'd use docker-compose@-srv-swag.service
.
So if we provide a directory path as the instance name when enabling our service, systemd will run the docker compose
commands in that directory, because the value of WorkingDirectory
is the %I
placeholder.
This means we can use the same service file for multiple Docker Compose stacks.
The [Install]
section
Finally, we need to tell systemd when to trigger our service.
This is done in the [Install]
section.
The WantedBy
directive defines a target unit that our service will be a part of when it is enabled.
In systemd, the multi-user
target is the default target that the system will activate, so we use this.
Using our new service
This is the easiest part. Now all we need to do is enable our service. Enabling a service means telling systemd to start it when the target that wants it is started. So if we enable our new service, systemd will start it when the system boots, because the default target wants it.
But we can't just systemctl enable docker-compose@.service
.
We need to provide an instance name.
Remember that the instance name is the escaped directory where our docker-compose.yml
file is located.
For example, let's enable the Secure Web Application Gateway stack, which is located in /srv/swag
. First, we'll escape the path:
$ systemd-escape /srv/swag
-srv-swag
Then we'll use the escaped path as the instance name. Note that systemctl
must be run as root.
# systemctl enable docker-compose@-srv-swag.service
Created symlink /etc/systemd/system/multi-user.target.wants/docker-compose@-srv-swag.service → /etc/systemd/system/docker-compose@.service.
Now our service is enabled and it will start next time we reboot the system. You can repeat this process for other Docker Compose stacks by using different directory paths.
Note: enabling a service does not start it. If you want to enable it and start it at once, you can use systemctl enable --now <service>
.
No Comments