Docker on VM with Traefik + Homepage
Overview
Deploy Docker services on the VM (Pop!_OS). Web services go behind Traefik (port 80) and register on Homepage (dashboard at /).
- Access domain:
vm.local(mDNS) — use for appCMD_DOMAIN/BASE_URL, NOTvm - Network:
traefik-proxy(external bridge) — all web containers must join - Crane:
~/go/bin/crane(NOT on PATH) - Compose:
docker compose(plugin at~/.docker/cli-plugins/). Mustcdinto service dir —-fflag broken
Architecture
Port 80 → Traefik ──┬── / → Homepage (dashboard)
├── /hedgedoc → HedgeDoc (:3000) [subpath-aware]
├── /freshrss → FreshRSS (:80)
├── /traefik → Traefik dashboard (api@internal)
└── /<service> → Your new service
Direct ports ───────── :6060 → BookLore [no subpath support]
Config: ~/traefik/traefik.yml — static config
~/traefik/dynamic/<service>.yml — per-service routing
~/homepage/config/services.yaml — dashboard entries
Deploy Checklist
- Prepare —
mkdir -p ~/docker/<service>/{data,config} - Pull —
~/go/bin/crane pull <image>:<tag> /tmp/<name>.tar - Load —
docker load -i /tmp/<name>.tar - Compose — Write
~/docker/<service>/docker-compose.yml→cd ~/docker/<service> && docker compose up -d - Traefik — Create
~/traefik/dynamic/<service>.yml(auto-reloads) - Validate before writing configs — curl-test EVERY URL/route before hardcoding into config files (see Validation section)
- Homepage — Add entry to
~/homepage/config/services.yaml+ group insettings.yaml - Verify —
curl -sI --noproxy '*' http://localhost/<service>
Validation (MUST DO before writing configs)
Always curl-test links before hardcoding them. Do not assume a route works — prove it.
# 1. Test container responds directly (find internal IP or use exposed port)
curl -sI --noproxy '*' http://127.0.0.1:<host-port>/
# 2. Test Traefik route works (after writing dynamic config, before Homepage)
curl -sI --noproxy '*' http://127.0.0.1/<service>/
# 3. Test subpath: check HTML AND assets (not just the first response)
curl -s --noproxy '*' http://127.0.0.1/<service>/ | grep -E '<base href|<script src|<link.*href'
# If asset paths are absolute (e.g. /styles.css not ./<service>/styles.css) → subpath won't work
# 4. Test Homepage href URL resolves (the exact URL that will go in services.yaml)
curl -sI --noproxy '*' <href-value>
# 5. Test widget URLs (e.g. Traefik widget)
curl -s --noproxy '*' http://traefik:8080/api/overview
Rule: If curl returns 404, 502, or broken assets → fix FIRST, then write the config.
Key Gotchas
| Gotcha | Detail |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Container name | Use http://<container>:<port> in Traefik config, NOT localhost |
| Sub-path + stripPrefix | Need BOTH: stripPrefix in Traefik AND app's BASE_URL/CMD_URL_PATH. Test CSS/JS assets, not just HTML. BUT some SPAs (e.g. Booklore) don't support subpath at all — use direct port instead |
| Subpath compatibility | Before deploying behind subpath, check if the app supports it. SPAs with hardcoded <base href="/"> and no BASE_PATH config will break. Use direct host port + Homepage href: http://172.16.252.7:<port> as fallback |
| CMD_DOMAIN must match access IP | For apps like HedgeDoc with CMD_DOMAIN, set it to the IP/hostname users actually use (e.g. 172.16.252.7), NOT vm.local if users access via IP |
| No host ports | Don't expose ports if Traefik handles routing. Exception: apps without subpath support MUST expose host ports |
| Traefik dashboard needs 2 routers | Dashboard at /traefik uses stripPrefix + api@internal. But dashboard JS calls /api/... hardcoded — need a second router PathPrefix(/api) → api@internal (no strip). Without it, API calls fall through to Homepage and dashboard shows empty data. Do NOT use api.basePath — it breaks the insecure port API and the Homepage widget |
| Priority 10 | Set on all routers so Homepage / catch-all still works |
| Auth | auth@file for BasicAuth — omit for public services |
| Volume paths | Always absolute: /home/htlin222/docker/<service>/... |
Deep-Dive Docs
| Topic | File |
| ------------------------------------------------ | ------------------- |
| Templates (compose, traefik, homepage, makefile) | @templates.md |
| Troubleshooting | @troubleshooting.md |
| Reference stacks (ready-to-deploy) | references/*.yml |
Homepage Groups
| Group | Current Services |
| -------------- | --------------------------------------------------------- |
| Media | Booklore (direct port :6060, no subpath support) |
| Productivity | HedgeDoc (subpath via Traefik, CMD_DOMAIN=172.16.252.7) |
| Infrastructure | Traefik |