Skip to main content

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>.