Instruction file imported from LukasParke/swarm (
.cursor/rules/swarm-stack-conventions.mdc). Copyright stays with the author.
Docker Swarm Stack Conventions
Reverse Proxy: Traefik
This swarm uses Traefik as the reverse proxy. All swarm services that need HTTP/HTTPS access must be routed through Traefik via labels. The domain pattern is <service>.parke.dev.
Exposing a Service via Traefik
- Add the Traefik overlay network to the service and to the stack's
networkssection as external. - Add Traefik deploy labels with HTTPS + HTTP redirect routers.
- Do NOT publish ports directly — Traefik handles ingress.
Service-level config:
services:
my-app:
networks:
- traefik_proxy
deploy:
labels:
- "traefik.enable=true"
# HTTPS router
- "traefik.http.routers.<name>.rule=Host(`<name>.parke.dev`)"
- "traefik.http.routers.<name>.entrypoints=websecure"
- "traefik.http.routers.<name>.tls=true"
# HTTP router (redirect to HTTPS)
- "traefik.http.routers.<name>-http.rule=Host(`<name>.parke.dev`)"
- "traefik.http.routers.<name>-http.entrypoints=web"
- "traefik.http.routers.<name>-http.middlewares=https-redirect@swarm"
# Service port
- "traefik.http.services.<name>.loadbalancer.server.port=<container-port>"
External network definition (required in every stack that uses Traefik):
networks:
traefik_proxy:
external: true
name: traefik_traefik_proxy
Traefik Conventions
- TLS is set per-router (not globally).
- HTTP->HTTPS redirect is handled per-router via the shared
https-redirect@swarmmiddleware defined on the Traefik service. - Router names must be unique across the entire swarm. Use the service name as prefix (e.g.
termix,termix-http). - Do not expose ports via
ports:if the service is only accessed through Traefik.
NFS Volumes via Subpath
Stacks running on Percy or Hercules (Docker 28+) use a single nfs-data volume pointing to the NFS root, then volume.subpath to mount subdirectories.
services:
my-app:
volumes:
- type: volume
source: nfs-data
target: /app/data
volume:
subpath: <stack-name>/data
volumes:
nfs-data:
driver: local
driver_opts:
type: nfs
o: "addr=10.10.10.215,rw,nfsvers=3,soft"
device: ":/mnt/user/swarm-data"
- The NFS root always exists, so the volume mount never enters a failed state.
- Requires Docker Engine 28+ (subpath was broken in Swarm mode before 28).
Apollo Node (Unraid NAS)
Apollo runs Docker 27.5.1 (Unraid) and cannot use volume subpath. Stacks on Apollo (qflood, lidarr, radarr, sonarr) use local bind mount volumes:
volumes:
app-config:
driver: local
driver_opts:
type: none
o: bind
device: /mnt/user/swarm-data/<stack>/config
The *arr apps use /mnt/user/appdata/<app>/ paths. QFlood uses /mnt/user/swarm-data/qflood/ paths. Both are local to the Unraid NAS.
All stacks using NFS subpath must exclude Apollo:
deploy:
placement:
constraints:
- node.hostname != Apollo
All three nodes are managers, so node.role == manager does NOT exclude Apollo. Always use node.hostname != Apollo instead.
GitOps: GitHub Actions
A self-hosted GitHub Actions runner (github-runner/) runs inside the swarm as the bootstrap stack. It connects outbound to GitHub — no inbound ports or SSH required.
- Workflow:
.github/workflows/deploy.ymltriggers on push tomainor manual dispatch - Runner: Uses label
swarm-deployer, constrained tonode.hostname != Apollo - Secrets: VPN credentials stored as GitHub Actions secrets, exported as env vars during deploy
- Adding a stack: Create the directory + compose file, add the stack name to the
stacksarray in the workflow, push tomain - The runner does not manage itself —
github-runner/is deployed manually (bootstrap)
Environment Variables
- Secrets use
${VARIABLE}syntax in compose files, substituted from GitHub Actions secrets during deploy - Hardcode non-sensitive values (hostnames, ports, paths, feature flags) directly in the compose file
.envfiles are gitignored and only used for local/manual deploys
Host-Mode Ports
Services using mode: host ports must include a placement constraint to pin them to a specific node by hostname.
Service Deploy Defaults
All services should include:
deploy:
replicas: 1
restart_policy:
condition: any
delay: 5s
max_attempts: 0
window: 120s
update_config:
parallelism: 1
delay: 10s
order: stop-first