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.

Written Monday, 11 December 2023

Tagged with linux.

Categorized as “

What do you think of this post?