Using docker compose under systemd
Monday, 11 December 2023
This is a revision of my 2021 post about the same thing. The main change is how I define and use the docker containers with a templated systemd service and docker-compose.yml files instead of calling docker directly with many arguments.
Running docker-compose through systemd
I wrote a simple systemd template service that calls into docker compose. Placed in /etc/systemd/system/docker-service@.service, the file looks like this
[Unit] Description=%i Docker service After=docker.service docker-traefik-network.service Requires=docker.service docker-traefik-network.service AssertPathExists=/usr/local/etc/containers/%i/docker-compose.yml [Service] TimeoutStartSec=0 Restart=always WorkingDirectory=/usr/local/etc/containers/%i ExecStart=/usr/bin/docker-compose --file /usr/local/etc/containers/%i/docker-compose.yml --project-name traefik up --force-recreate ExecStop=/usr/bin/docker-compose --file /usr/local/etc/containers/%i/docker-compose.yml --project-name traefik down [Install] WantedBy=multi-user.target
docker-traefik-network is another service, which ensures that the externally managed traefik network exists for any of the services that want to use it. The service file looks like this
[Unit] Description=Create treafik network After=docker.service Requires=docker.service [Service] Type=oneshot ExecStart=/bin/bash -c "/usr/bin/docker network create traefik ||:" ExecStop=/usr/bin/docker network rm traefik RemainAfterExit=yes [Install] WantedBy=multi-user.target
This ends up being pretty easy to use, just make a /usr/local/etc/containers/{service} folder for each service you wish to run, with a docker-compose.yml file and any other files you might need to mount in as configuration adjacent to it.
Here is an example of how I configured pihole in this way, in /usr/local/etc/containers/pihole/docker-compose.yml:
---
version: "3.8"
networks:
traefik:
external: true
name: traefik
services:
pihole:
image: pihole/pihole
networks:
- traefik
security_opt:
- seccomp=unconfined
- apparmor=unconfined
environment:
- "TZ=America/Vancouver"
- "PIHOLE_DNS_=208.67.222.222;208.67.220.220"
- "WEBPASSWORD=admin"
labels:
traefik.enable: true
traefik.http.routers.pihole-http.rule: Host(`pihole.home.arpa`)
traefik.http.routers.pihole-http.entrypoints: web
traefik.http.routers.pihole-http.service: pihole-http-service
traefik.http.services.pihole-http-service.loadbalancer.server.port: 80
traefik.http.middlewares.pihole.addprefix.prefix: /admin
ports:
- 53:53/udp
- 53:53/tcp
volumes:
- /ssd-storage/pihole/etc/pihole:/etc/pihole
- /ssd-storage/pihole/etc/dnsmasq.d:/etc/dnsmasq.d
restart: unless-stopped
as well as traefik, which is the glue holds my entire setup together, found in /usr/local/etc/containers/traefik.yml
---
version: "3.8"
networks:
traefik:
external: true
name: traefik
services:
traefik:
image: traefik:v2.9
container_name: traefik
networks:
- traefik
environment:
- PUID=1000
- PGID=1000
- TZ=America/Vancouver
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--api.dashboard=true"
- "--api.insecure=true"
labels:
traefik.enable: true
traefik.http.routers.api.rule: Host(`traefik.home.arpa`)
traefik.http.routers.api.entrypoints: web
traefik.http.routers.api.service: api@internal
ports:
- 80:80
volumes:
- /storage/traefik/letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
I have yet to worry about setting up SSL internally for this but it is next on my list of things to figure out.
Although not shown above; a nice benefit of this is that using a separate docker-compose.yml for each service allows you to run additional services needed for certain applications and keep them bound inside an internal network of each docker-compose environment. I will end this section with an example of my nextcloud configuration which has a few associated services and was a major reason for deciding to restructure how I managed the various services I run.
---
version: "3.8"
networks:
external:
internal:
internal: true
traefik:
external: true
name: traefik
services:
app:
depends_on:
- postgres
- redis
environment:
- POSTGRES_DB=nextcloud
- POSTGRES_USER=nextcloud
- POSTGRES_PASSWORD=not-my-password
- POSTGRES_HOST=postgres
- REDIS_HOST=redis
expose:
- 9000
hostname: nextcloud.home.arpa
image: nextcloud:stable-fpm-alpine
networks:
- internal
- external
restart: unless-stopped
volumes:
- /storage/nextcloud/webapp-data:/var/www/html
cron:
depends_on:
- postgres
entrypoint: /cron.sh
image: nextcloud:stable-fpm-alpine
restart: unless-stopped
volumes:
- /storage/nextcloud/webapp-data:/var/www/html
postgres:
environment:
- POSTGRES_USER=nextcloud
- POSTGRES_PASSWORD=not-my-password
- POSTGRES_DB=nextcloud
- PGDATA=/var/lib/postgresql/data
expose:
- 5432
image: postgres:15-alpine
networks:
- internal
restart: unless-stopped
volumes:
- /storage/nextcloud/pg-data:/var/lib/postgresql/data
redis:
expose:
- 6379
image: redis:alpine
networks:
- internal
restart: unless-stopped
web:
depends_on:
- app
image: nginx:alpine
labels:
traefik.enable: true
traefik.docker.network: traefik
traefik.http.routers.nextcloud-http.entrypoints: web
traefik.http.routers.nextcloud-http.rule: Host(`nextcloud.home.arpa`)
traefik.http.routers.nextcloud-http.service: nextcloud-http-service
traefik.http.services.nextcloud-http-service.loadbalancer.server.port: 80
networks:
- traefik
- internal
restart: unless-stopped
volumes:
- /usr/local/etc/containers/nextcloud/nginx.conf:/etc/nginx/nginx.conf:ro
- /storage/nextcloud/webapp-data:/var/www/html:ro
Closing remarks
I am starting to strongly consider if it would have been easier to just run this through Docker Swarm or Hashicorp Nomad or some other orchestrator but that seems overkill for a single-server solution to running some containers. This setup still seems simple enough and I’ve been quite happy with how reliable it has been.