Improving the Process

At this point, we have a small handful of containers running, and are likely looking at adding more to the list. It's only reasonable to consider the maintainability of the resulting system, and the fact of the matter is, the prospects of keeping going with individual containers on the swarm are poor. A better solution is needed. Docker of course offers Compose, but that's more of a development tool. It doesn't persist across restarts of the runtime, doesn't scale to multiple worker hosts, if that's the direction we are interested in, On the other hand, it does manage groups of Docker images well, allows expressing dependencies between containers, and can manage both stock and custom images. Fortunately, Docker provides the Docker Stack system to leverage Compose across a swarm, allowing us the best of both worlds.

First, we'll install docker compose.

$ paru -S docker-compose

We need to set up the compose file. Unfortunately, Docker Stack does not support include yet, so we have to maintain a giant docker-compose file. Create docker-compose.yml with the following contents. Note that these compose files will not work in Docker Compose - the labels are defined for stack deployments.

networks:
  homelab:
    driver: overlay
    ipam:
      config:
        - subnet: 10.64.0.0/16
    internal: true
  homelab-bridge:
    driver: overlay
    ipam:
      config:
        - subnet: 10.96.0.0/16

secrets:
  percona-root:
    file: /home/andreas/secrets/percona-root
  ghost:
    file: /home/andreas/secrets/ghost

configs:
  ghost-config:
    file: /home/andreas/configs/config.production.json
  traefik-config:
    file: /home/andreas/configs/traefik.toml

services:
  traefik:
    configs:
      - source: traefik-config
        target: /etc/traefik/traefik.toml
    deploy:
      labels:
        traefik.enable: "true"
        traefik.http.routers.traefik-rtr.entrypoints: "websecure"
        traefik.http.routers.traefik-rtr.middlewares: "traefik-allowlist@file"
        traefik.http.routers.traefik-rtr.rule: "Host(`traefik.turriff.net`)"
        traefik.http.routers.traefik-rtr.service: "api@internal"
        traefik.http.services.dummy-svc.loadbalancer.server.port: "9999"
      replicas: 1
    healthcheck:
      test: ["CMD","traefik","healthcheck"]
      interval: 30s
      timeout: 10s
    image: "traefik:3.0"
  logging:
    driver: journald
  networks:
    - homelab
    - homelab-bridge
  ports:
    - published: 80
      target: 10080
      protocol: tcp
      mode: host
    - published: 443
      target: 10443
      protocol: tcp
      mode: host
  restart: on-failure
  volumes:
    - "/srv/data/docker/traefik/rules:/rules:ro"
    - "/srv/data/docker/traefik/acme:/acme:rw"
    - "/srv/data/docker/traefik/logs:/logs:rw"
    - "/run/docker.sock:/run/docker/sock:ro"

  percona:
    deploy:
      replicas: 1
    environment:
      MYSQL_ROOT_PASSWORD_FILE: "/run/secrets/percona-root"
      PERCONA_TELEMETRY_DISABLE: 1
    hostname: "percona"
    image: "percona:ps-8"
    logging:
      driver: journald
    networks:
      - homelab
    restart: on-failure
    secrets:
      - percona-root
      - ghost
    volumes:
      - "/srv/data/docker/percona/data:/var/lib/mysql:rw"

  ghost:
    configs:
      - source: ghost-config
        target: /var/lib/ghost/config.production.json
    depends_on:
      - percona
    deploy:
      labels:
        traefik.enable: "true"
        traefik.http.routers.ghost-rtr.entrypoints: "websecure"
        traefik.http.routers.ghost-rtr.rule: "Host(`ghost.turriff.net`)"
        traefik.http.routers.ghost-rtr.service: "ghost"
        traefik.http.services.ghost.loadbalancer.server.port: "2368"    
      replicas: 1
    image: "ghost:5"
    logging:
      driver: journald
    networks:
      - homelab
    restart: on-failure
    volumes:
      - "/srv/data/docker/ghost/data:/var/lib/ghost/content:rw"

This does pretty much the same our assorted docker service create and docker service update commands did earlier. With the file set up, let's clean up the individually managed services and get the stack running

$ docker service rm ghost
$ docker service rm percona
$ docker service rm traefik
$ docker config rm ghost
$ docker config rm traefik
$ docker secret rm ghost
$ docker secret rm percona-root
$ docker stack deploy -c docker-compose.yml --prune homelab

The stack will check for updates every time it's brought online. To account for this, we will avoid using "latest" tags fpr our images and explicitly specify at least major versions. Even with the giant compose file, this should make managing the home lab a lot easier.

Updating images is easily accomplished with

$ docker compose -f docker-compose.yml pull
$ docker stack deploy -c docker-compose.yml --prune homelab

Subscribe to Homelab Adventures

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe