Start Docker Compose using systemd on Debian

How do you start up your multi-container application? Lots of the time these multi-container setups have dependencies between them and you need to start them in a specific order? How can you get this done automatically on bootup? Today I’m going over how to use Docker Compose and systemd to automatically launch all your containers in the correct order on bootup leveraging systemd on a Debian host.

So if you’ve got an MQTT broker (or another service) that must start up before your Home Assistant service and you’re already using Docker Compose this is an article for you! This method works great on a Raspberry Pi but should also work on anything using systemd as the initilization system.

wait-for-it

First off, there are probably dependencies between services in your docker-compose.yml file. For the purposes of this article I’m going to detail the method I used for my Home Assistant configuration. My compose file looks like:

If you’re interested in learning more about some of these services, check out my other articles:

Looking at this compose file you can see that both the grafana and homeassistant service depend on the influxdb service. When our system boots up we want to make sure they get started in the correct order and the next one doesn’t start until the previous is up and running. Ideally, our application logic takes care of all this but it’s a good idea to built-in checks to the startup logic.

To take care of the service dependency mechanism, I’m going to be using wait-for-it, a bash script that will wait until it sees data on a port. Go ahead and download that script and mark it as executable.

Service Script

Next, we’ll create the service script that systemd will call to start and stop our multi-container Docker application.

I created a new shell script called “service” (no file extension).

Let’s break this down:

  • First, we set the DIR variable to the directory that holds this script. I’m assuming this is in the same directory as the wait-for-it.sh script mentioned earlier as well as your docker-compose.yml file.
  • Next, we set the WAIT variable to the path of the wait-for-it.sh script
  • Two functions are defined for starting and stopping the application
    • start is where the startup happens. First a service is started using the docker-compose command and then the wait-for-it.sh script is called with the IP address and port to listen on. That script blocks until that port becomes active signaling the service is up and running. Change this section to match the services, ports, and order that make sense for your application.
    • stop just defines how to stop the application. In my case, I’m just letting docker-compose manage this for me. You could shut down services in a particular order if that makes sense for your setup.
  • Finally, there is a case statement that reads the first argument of the script and decides if it was meant to start or stop the application. If nothing is passed in the application is started. If something else was passed in an error message is printed out.

After creating this file you should mark it as executable (chmod +x service). To test it out run ./service start to start the application and ./service stop to bring it down.

Service File

Debian and many other Linux distributions use systemd as their initialization system. We can create a systemd service for our docker application so that the operating system can start it up on a reboot.

To create a systemd service you need a .service file. For this example, I created a new file called ha.service. See the contents below:

What does this do?

  • The description sets the description of the service, I just named my Home Assistant because that’s what my docker-compose.yml is bringing up
  • ExecStart is the command to run to start the service. Here I call the service bash script I created in the earlier section. start is passed in as the first argument so that it brings up the application. You need to change this to wherever you are storing your service script.
  • ExecStop is the command to stop the service. Again, stop is passed into the script so it shuts stuff down correctly.
  • RemainAfterExit is set to yes. This tells systemd that even though my service script exited the service is still active. Our service script exits after all the docker containers are spun up.
  • WantedBy lets systemd know when the service should be started on reboot. Setting it to multi-user.target tells systemd to only start the service once the machine is basically done booting up. Then we know network connections and other services we depend on have already been started.

After creating that file we need to enable it. I symlinked it into the /etc/systemd/system directory and then enabled it through systemctl.

Testing it Out

Everything should now be set up for a proper systemd service. Reboot your server to make sure that everything starts up correctly. You can also use systemctl to start and stop the service manually.

If you need help debugging, you can use journalctl to view the systemd logs. The following command usually works for me (make sure you scroll to the bottom):

At first, systemd looks like it’s a really complicated solution for getting your application running after reboot, but hopefully breaking it down like this has cleared up some of the mystery. Let me know in the comments of other great systemd best practices or how you bootup your docker applications.

Additional Links