Docker Swarm

Home

Five-node Swarm cluster managed from PCT 108 (network, 192.168.2.82). PCT 109 (ai) is not a Swarm node — all its services are standalone Docker Compose.

Node Hostname IP Role
PCT 101 downloads 192.168.2.190 Worker
PCT 102 media-core 192.168.2.191 Worker
PCT 104 documents 192.168.2.105 Worker
PCT 107 debian 192.168.2.81 Worker
PCT 108 network 192.168.2.82 Manager

Key Commands

All Swarm commands run from the Proxmox host via pct exec 108 --.

# Status
pct exec 108 -- docker node ls
pct exec 108 -- docker stack ls
pct exec 108 -- docker service ls

# Inspect a service
pct exec 108 -- docker service ps <service>              # failed replica history
pct exec 108 -- docker service logs <service> --tail 50

# Deploy / redeploy
pct exec 108 -- docker stack deploy -c <compose> <stack>
pct exec 108 -- docker service update --force <service>  # restart + fresh overlay IP

# Check containers on a specific node
pct exec 102 -- docker ps -a --filter name=<name>
pct exec 104 -- docker ps

Compose Files

Stored in Portainer on PCT 108:

/mnt/tank/appdata/portainer/compose/<id>/docker-compose.yml

Portainer env var gotcha: Portainer stores stack env vars in its internal DB (portainer.db) — there are no .env files on disk. When redeploying via CLI, export vars manually:

pct exec 108 -- bash -c 'export VAR=val && docker stack deploy -c <compose> <stack>'

Portainer Directory → Stack Mapping

Dir ID Stack
2 documents-linkwarden
4 documents-paperless
6 network-guacamole
7 documents-nextcloud
10 documents-immich
22 documents-homarr
23 authentik
25 dozzel
26 media-core
28 documents-actual-budget
31 network-cloudbeaver
32 romm

Swarm Labels vs Container Labels

In Docker Swarm, only deploy.labels are read by Traefik — container-level labels: are ignored.

services:
  myservice:
    deploy:
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.myservice.rule=Host(`myservice.carr-family.org`)"
        - "traefik.http.services.myservice.loadbalancer.server.port=8080"

Cross-Provider Middleware Reference

Middlewares defined in routes.yml (file provider) must be referenced with @file suffix in Swarm labels:

- "traefik.http.routers.myservice.middlewares=lan-only@file"

Plain names default to @swarm and return 404.


Overlay Network Health Check

Services on PCT 102 can develop stale VXLAN attachments after redeployments, causing 504s even though the service shows 1/1 and the container is healthy.

Diagnose:

TRAEFIK=$(pct exec 108 -- docker ps -q --filter name=traefik)
IP=$(pct exec 102 -- docker inspect <container-name> --format '{{(index .NetworkSettings.Networks "traefik-public").IPAddress}}')
pct exec 108 -- docker exec $TRAEFIK ping -c 2 $IP

Fix:

pct exec 108 -- docker service update --force <service-name>

All 11 media-core services were force-updated 2026-05-13 to clear a full-stack stale attachment event.


Standalone Containers (not Swarm-managed)

Several services run as docker compose or docker run directly on specific nodes, outside Swarm. They need static routes in Traefik's routes.yml since Swarm label discovery doesn't apply.

Container Node IP:Port Route
qbittorrent-mam PCT 101 192.168.2.190:8080 static route
qbittorrent-vpn PCT 101 192.168.2.190:8081 static route
gluetun-proton PCT 101 VPN gateway only
qui PCT 101 192.168.2.190:7476 static route
jellyfin PCT 101 192.168.2.191:8096 static route
otterwiki PCT 104 192.168.2.105:8081 static route
litellm + postgres PCT 109 192.168.2.83:4000 static route
n8n + qdrant PCT 109 192.168.2.83:5678 static route
openclaw PCT 109 192.168.2.83:18789 static route
odysseus PCT 109 192.168.2.83:7000 static route
firecrawl PCT 109 192.168.2.83:3002 no route
whisper PCT 109 192.168.2.83:8001 no route
proton-bridge PCT 109 192.168.2.83:1025/1143 no route
Pterodactyl Panel PCT 300 192.168.2.136:80 static route (lan-only)

General Compose Notes

  • All services use PUID=1000 PGID=1000 and TZ=America/Toronto unless noted
  • gai_conf_ipv4 is an external Docker config used by Prowlarr to force IPv4 DNS
  • Before adding bind mounts: mkdir -p /tank/appdata/<app>/<dir> && chown 1000:1000 ... on the host first
  • PCT 108 cannot read /tank/appdata/<app>/ files directly from other nodes' compose dirs — always pipe compose files in via stdin when deploying from a compose file that lives on another node