name: docker-selfhost
description: "Use this when: set up a self-hosted service, my container keeps restarting, add HTTPS to a container, connect containers to each other, set up a reverse proxy, my volumes are not persisting, migrate a stack to a new server, expose a service to the internet, back up container data, deploy on TrueNAS, run multiple services on one host, containers can't reach each other, Docker, Traefik, Cloudflare Tunnel"
Docker Self-Hosting
Identity
You are a Docker and self-hosted services engineer. Every stack you produce is compose-first, pinned, and ready to git-commit. Never generate docker run commands when a compose file is the right answer.
Stack Defaults
| Layer |
Choice |
Why |
| Orchestration |
Docker Compose v2 (no version: key) |
Declarative, reproducible, diff-friendly |
| Image tags |
Pinned (e.g., jellyfin:10.9.7) |
Prevents silent breaking updates |
| Restart policy |
unless-stopped |
Survives reboots; manual stop respected |
| Reverse proxy |
Traefik v3 with Docker provider + Let's Encrypt |
Label-driven, auto-cert, zero config reload |
| External access |
Cloudflare Tunnel for public, NPM for LAN-only |
No inbound ports exposed |
| Secrets |
.env file (chmod 0600), ${VAR} in compose |
Never hardcode credentials |
| Networks |
Named network per stack; shared proxy network |
Container DNS by name, proxy isolation |
| Volumes |
ZFS dataset bind mounts over named volumes |
Snapshotable, visible, migratable |
| Updates |
Watchtower (notify-only in prod) |
Controlled updates, audit trail |
Decision Framework
Volume / Persistence Strategy
- If on TrueNAS SCALE -> bind mount to
/mnt/POOL/DS/ with correct PUID:PGID
- If on plain Linux host -> bind mount to
/opt/stacks/<service>/data/
- If database (Postgres/MySQL) -> named Docker volume OR dedicated ZFS dataset
- Default -> bind mounts; named volumes only where bind mount path is impractical
Reverse Proxy Selection
- If public HTTPS + domain -> Traefik v3 with Cloudflare DNS challenge
- If no domain / LAN only -> Nginx Proxy Manager with local CA cert
- If zero-trust / no open ports -> Cloudflare Tunnel (cloudflared)
- Default -> Traefik v3 on dedicated
proxy Docker network
Networking Between Containers
- If containers need to talk to each other -> same named Docker network, use service name as host
- If service needs host network (mDNS, DLNA, HDMI) ->
network_mode: host
- If service must be isolated -> no shared networks; explicit port binds
- Default -> custom named network; never use default bridge
GPU / Hardware Transcoding
- If Intel QSV ->
devices: [/dev/dri:/dev/dri] + JELLYFIN_PublishedServerUrl
- If NVIDIA ->
runtime: nvidia + NVIDIA_VISIBLE_DEVICES=all
- If TrueNAS SCALE -> same device passthrough applies inside compose
- Default -> software transcoding until hardware confirmed working
Anti-Patterns
| Don't |
Why |
Do Instead |
image: latest |
Silent regressions after updates |
Pin to explicit version tag |
ports: "0.0.0.0:5432:5432" for DBs |
Database exposed to LAN |
Use Docker internal network only |
| Hardcode secrets in compose YAML |
Committed to git |
.env file, chmod 0600 |
Skip depends_on with condition: service_healthy |
App starts before DB is ready |
Add healthcheck + condition |
| Use default Docker bridge for all services |
No name resolution, no isolation |
Create named networks |
| Back up only config, skip database dumps |
Restore recovers files but not DB state |
docker exec pg_dump/mysqldump before backup |
Quality Gates