This post summarizes what worked (and what hurt) when I deployed multiple self-hosted services in this repo (e.g. dockhand, vaultwarden, Checkmate, Miniflux). It focuses on repeatable defaults, pitfalls, and a practical debugging workflow.
Goals and non-negotiables
- Domain-only access: Do not expose host ports for business services. Avoid
IP:portbypassing auth or leaking admin panels. - Single entrypoint: Only open
80/443externally. Let Traefik handle reverse proxying, TLS, and security headers. - Shared middleware: Run shared MongoDB / Redis / PostgreSQL once and reuse them across apps to reduce waste.
- Never break userspace: Any change must not break existing online services (routing/port/network/auth conflicts).
Repo layout and my deployment defaults
- One app per directory: e.g.
dockhand/,vaultwarden/,Checkmate/,v2/(Miniflux). - Each app has a runnable
docker-compose.yml: avoid scattered upstream examples undercontrib/. - Shared middleware is a separate project:
middlewares/runspostgres/mongodb/redis.- Shared middleware joins internal networks only and exposes no public ports.
- Apps join shared networks via
externalnetworks.
- One Traefik network: all services that Traefik should reach join an external network like
traefik_proxy. - Prefer Docker labels (docker provider): use file provider only for a few non-container targets or special cases.
Standard deployment playbook (the core tricks)
1) Do not publish host ports
For public-facing services:
- Use
expose:(container-network visible only) instead ofports:(host visible). - Route traffic via Traefik using labels.
This immediately reduces:
- Port conflicts (e.g. host
3000already in use). IP:portbypass risks.
2) Traefik labels (domain routing + TLS + security headers)
Typical label set (example):
traefik.enable=truetraefik.http.routers.<name>.rule=Host(`example.com`)traefik.http.routers.<name>.entrypoints=websecuretraefik.http.routers.<name>.tls.certresolver=letsencrypttraefik.http.routers.<name>.middlewares=security-headers@filetraefik.http.services.<name>.loadbalancer.server.port=<container_port>
3) Multi-network containers must pin the Traefik network
When a container joins both:
traefik_proxy(for Traefik)infra_backend(for databases/caches)
Traefik may pick the wrong container IP (from a network Traefik cannot reach), resulting in 504 Gateway Timeout.
Fix: add this label to the service:
traefik.docker.network=traefik_proxy
This was the typical cause for Checkmate: adding it removed the 504 immediately.
Shared middleware (middlewares/)
Design principles
- High cohesion, low coupling: middleware provides network services only.
- Least privilege: no public ports by default; internal networks only.
- Centralized persistence: volumes managed in
middlewares/for easier backup/migration.
Practical notes
- Use a stable shared network name (e.g.
infra_backend), and let apps join withexternal: true. - Use service names in connection strings (e.g.
postgres/mongodb). - Prefer separate DB/schema/user per app to avoid data pollution.
App-specific pitfalls I hit this time
Dockhand (high-risk admin surface)
- Do not publish host ports, domain + Traefik only.
- Always add auth: protect it with Traefik
basicAuth. - Keep it simple; complexity is how incidents are born.
Vaultwarden (Bitwarden-compatible)
This is where it is easy to “be clever” and break clients:
- Do not put BasicAuth on the entire site: official clients and browser extensions will fail.
- Correct approach: protect
/adminonly.- Client paths like
/,/api,/identitymust not be blocked by BasicAuth.
- Client paths like
- If “the UI loads but login fails”, check:
- whether Cloudflare is challenging API requests (see below)
- whether
/apiis treated as ServerConfig (may require rewriting to/api/configin Traefik)
Checkmate (multi-network + MongoDB)
- Sharing MongoDB + multiple networks often triggers the Traefik wrong-network issue:
- symptoms: 504
- fix:
traefik.docker.network=traefik_proxy
Miniflux (rss reader)
- Miniflux requires PostgreSQL. No MongoDB/Redis is needed (unless you add RSSHub/Browserless, etc.).
HEAD /returning405can be normal as long asGETworks; do not misdiagnose it as downtime.
Cloudflare pitfall: clients/extensions cannot log in
I hit the same issue twice:
- Vaultwarden browser extension: Cloudflare challenge → 403
- Reeder (Google Reader API): Cloudflare challenge → 403
Common signals:
- response headers contain
server: cloudflare+cf-mitigated: challenge - requests never reach Traefik/app logs
Mitigation options (recommended order):
- A: set the subdomain to DNS-only (grey cloud): the most robust, no client-side hacks needed.
- B: keep proxy (orange cloud), but skip challenge for this host/path:
- at minimum allow
/api,/identity,/reader/api/0,/accounts/ClientLogin, etc.
- at minimum allow
Key takeaway:
- “It works in a browser” does not mean “it works in a client”. Browsers can pass challenges; clients usually cannot.
Debug playbook (fast to slow)
1) Decide whether the request reaches your server
- Cloudflare 403 challenge: request never reaches your server. Fix Cloudflare/WAF first.
- Traefik 404/504: request reaches Traefik, but routing/network/backend is wrong.
- Backend 4xx/5xx: request reaches the app. Inspect app logs/config.
2) Traefik-side debugging
- Check access logs: which router/service handled the request and what status code it returned.
- Multi-network containers: suspect missing
traefik.docker.networkfirst. - Same host defined twice: suspect file-provider vs docker-provider conflicts (delete legacy file routes).
3) App-side debugging
docker logs <container>: verify startup, listening port, DB connectivity.- Verify you did not accidentally use
ports:and expose/conflict host ports.
Final security checklist (before going public)
- No business ports published on the host (only keep
80/443) - Admin surfaces must be authenticated (dockhand, vaultwarden
/admin, etc.) - Databases/caches are never exposed to the public internet
- No Traefik routing conflicts (avoid defining the same host in both file-provider and docker-provider)
- Cloudflare must not challenge API paths (clients/extensions/third-party integrations will break)
Comments