Sam's blog
May 15, 2024

In-place rebuilding of a docker compose project with systemd

Posted on May 15, 2024  •  3 minutes  • 443 words

The usual approach

If you do a search about how to run docker compose with systemd, you get something similar to this:

[Unit]
Description=Some docker compose service
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=true
WorkingDirectory=/path/to/project
ExecStart=/usr/bin/docker-compose up --build -d
ExecStop=/usr/bin/docker-compose down

[Install]
WantedBy=multi-user.target

While this works, it has some issues:

There’s also no automated restarts if a container fails, but you should manage that with a restart policy on each service in the compose file .

A (slightly) better approach

One solution is to turn it into a regular unit so that we can use docker-compose up to attach to the confiners and print the logs:

[Unit]
Description=Some docker compose service
Requires=docker.service
After=docker.service

[Service]
WorkingDirectory=/path/to/project
# wait for the containers to come up before the service is considered "started"
ExecStartPre=/usr/bin/docker-compose up --build --wait
# attach to the containers and print logs
ExecStart=/usr/bin/docker-compose up
# prevent docker-compose from bringing down the containers when restarting
RestartKillSignal=SIGKILL

[Install]
WantedBy=multi-user.target

Note the exclusion of an ExecStop, instead we are relying on the SIGTERM sent when stopping the unit (this will gracefully stop the containers as expected).
This also lets us use SIGKILL when restarting the unit, which force stops our docker-compose up - preventing it from stopping the containers when the service is restarted.

Unfortunately, stopping this way causes docker-compose to terminate with exit code 130, which systemd considers a failure:

% systemctl stop something
% systemctl status something
× something.service - Some docker compose service
     Loaded: loaded (/usr/lib/systemd/system/something.service; enabled; preset: enabled)
     Active: failed (Result: exit-code) since Wed 2024-05-15 19:42:33 CAT; 54s ago
   Duration: 52.405s
    Process: 77957 ExecStart=/usr/bin/docker-compose up (code=exited, status=130)
   Main PID: 77957 (code=exited, status=130)
        CPU: 1.425s

This could be silenced by putting a - in front of the command so it reads ExecStart=-/usr/bin/docker-compose up.

Alternatively, you could use a bash script to ignore only the 130 exit code:

#!/usr/bin/env bash
/usr/bin/docker-compose up
exit $(( $? == 130 ? 0 : $? ))

And run it from the systemd unit like ExecStart=/path/to/script.sh

A better approach

The exit code shenanigans can be avoided if we used docker-compose logs for our main process instead:

[Unit]
Description=Some docker compose service
Requires=docker.service
After=docker.service

[Service]
WorkingDirectory=/path/to/project
ExecStartPre=/usr/bin/docker-compose up --build --wait
ExecStart=/usr/bin/docker-compose logs --follow -n 0
ExecStop=/usr/bin/docker-compose down
ExecReload=/usr/bin/docker-compose up --build --wait

[Install]
WantedBy=multi-user.target

This has the upside of keeping the expected behaviour of systemctl restart <service> (“stop and then start again”).

The service can be reloaded using systemctl reload <service>. Which will run ExecReload without stopping the service beforehand.

Links