| name | vm-docker |
| description | Deploy Docker services on VM with Traefik and Homepage. Use for VM container deploys. |
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 app CMD_DOMAIN/BASE_URL, NOT vm
- 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/). Must cd into service dir — -f flag 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 in settings.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.
curl -sI --noproxy '*' http://127.0.0.1:<host-port>/
curl -sI --noproxy '*' http://127.0.0.1/<service>/
curl -s --noproxy '*' http://127.0.0.1/<service>/ | grep -E '<base href|<script src|<link.*href'
curl -sI --noproxy '*' <href-value>
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 |