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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
version: "3" services: homeassistant: image: homeassistant/raspberrypi3-homeassistant:0.76.2 volumes: - /home/pi/home-assistant-config/configuration:/config - /etc/localtime:/etc/localtime:ro network_mode: "host" ports: - "8123:8123" devices: - "/dev/ttyACM0:/dev/zwave:rmw" depends_on: - influxdb - mosquitto influxdb: image: martin211/rpi-influxdb:1.5.0-1 volumes: - /var/influxdb:/data network_mode: "host" ports: - "8086:8086" environment: - "PRE_CREATE_DB=home_assistant" grafana: image: fg2it/grafana-armhf:v5.0.4 volumes: - /var/grafana:/var/lib/grafana network_mode: "host" ports: - "3000:3000" depends_on: - influxdb mosquitto: image: leojrfs/mosquitto-arm:latest volumes: - /var/mosquitto/data:/mosquitto/data - /var/mosquitto/log:/mosquitto/log - ./mosquitto.conf:/mosquitto/config/mosquitto.conf network_mode: "host" ports: - "1883:1883" - "9001:9001" |
If you’re interested in learning more about some of these services, check out my other articles:
- Setting up MQTT Broker for DIY Home Assistant Sensors
- Storing Home Assistant Sensor Data in InfluxDB
- Visualizing Data using Grafana
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.
1 2 |
wget "https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh" chmod +x wait-for-it.sh |
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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#!/usr/bin/env bash # # Bash script to startup all components of home assistant, mostly through # docker and checking if services are available using wait-for-it declare -r DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" declare -r WAIT="${DIR}/wait-for-it.sh" start() { cd ${DIR} docker-compose up -d influxdb ${WAIT} localhost:8086 docker-compose up -d grafana ${WAIT} localhost:3000 docker-compose up -d mosquitto ${WAIT} localhost:9001 docker-compose up -d homeassistant ${WAIT} --timeout=120 localhost:8123 } stop() { cd ${DIR} docker-compose stop } case $1 in start) start;; stop) stop;; "") start;; *) echo "Usage: ./service start|stop" esac |
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 thewait-for-it.sh
script mentioned earlier as well as yourdocker-compose.yml
file. - Next, we set the
WAIT
variable to the path of thewait-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 thedocker-compose
command and then thewait-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 lettingdocker-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:
1 2 3 4 5 6 7 8 9 10 11 12 |
# systemd service for Home Assistant using docker-compose [Unit] Description=Home Assistant [Service] ExecStart=/home/pi/home-assistant-config/service start ExecStop=/home/pi/home-assistant-config/service stop RemainAfterExit=yes [Install] WantedBy=multi-user.target |
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. Ourservice
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
.
1 2 3 4 5 |
# Symlink the service file to the correct location sudo ln -s /home/pi/home-assistant-config/ha.service /etc/systemd/system/ha.service # Enable the service sudo systemctl enable ha.service |
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.
1 2 3 4 5 |
# Stop the service sudo systemctl stop ha # Start the service sudo systemctl start ha |
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):
1 |
sudo journalctl -f -b |
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.