Blog
Traefik Reverse Proxy with Automatic HTTPS Certificates
Once you've got more than a handful of services, letting every container map its own 443 becomes a mess. Traefik sits at the edge: it owns 80/443, routes by hostname to the right backend, and mints Let's Encrypt certificates as a side effect. Compared to hand-writing Nginx server blocks, Docker labels stay glued to the container lifecycle; compared to Caddy, I just know Traefik's middleware chain better. This isn't an install-configure-deploy walkthrough — it's a working Traefik config taken apart piece by piece, with what each line does and what breaks if you drop it.
Entrypoints: where traffic comes in
command:
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
web and websecure are just names, but they show up in every route's entrypoints= from here on. Port 80 isn't only for humans — it's usually where the ACME HTTP-01 challenge happens; 443 is the real business entry. In a homelab, if 1Panel or another panel wants 80/443 too, one of you has to be the edge and the other an upstream. When two things fight over 443, cert failures usually surface as "browser complains about the certificate" while the log is actually showing a bind failure. When you set up the HTTP-to-HTTPS redirect, hang the redirect on web too, so users don't linger on the plaintext entry.
The container's ports maps host 80/443 in, so make sure nothing else on the host is silently listening on the same port. ss -lntp | grep -E ':80|:443' is the ritual before touching config. If IPv6 is enabled, confirm the entrypoint listens on v6 as well — otherwise you get the fake outage where "my phone works on cellular, but v6-preferring home connections can't reach it."
Docker provider: who qualifies for routing
- --providers.docker=true
- --providers.docker.exposedbydefault=false
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
exposedbydefault=false is the line I'd fight hardest to keep in this config. With default exposure, a container you forgot to label can still get swept into the route table. Read-only mounting docker.sock shrinks the attack surface a bit, but the socket is still a high-privilege channel — SSH and system updates on the Traefik host stay non-negotiable.
A business service has to opt in explicitly:
labels:
- traefik.enable=true
- traefik.http.routers.app.rule=Host(`app.example.com`)
- traefik.http.routers.app.entrypoints=websecure
- traefik.http.routers.app.tls.certresolver=le
- traefik.http.services.app.loadbalancer.server.port=8080
rule with Host() is the common case; multiple domains on one container means chaining Host() with a logical OR. server.port is the port inside the container, not the host mapping. Forget it and Traefik sometimes guesses, sometimes silently points at the wrong thing — you'll see 502s while the app logs stay perfectly clean. Check labels first, app second.
Certificatesresolvers: where certificates come from
- [email protected]
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
le is the name referenced later by tls.certresolver=le. Persist the directory holding acme.json, chmod it to 600, and treat losing it as a mass re-issue. HTTP-01 requires public port 80 to reach this entrypoint; the three classic failure causes are the domain not pointing at this host, a CDN orange-cloud blocking the challenge path, and port 80 being occupied. When troubleshooting, read the ACME section in Traefik's logs — don't just refresh the browser.
TLS-ALPN challenges suit setups that don't want to fuss with port 80, but HTTP-01 is more straightforward on home networks and with some tunnel combos. Renewal is automatic as long as 80/443 stay in Traefik's hands; well before expiry, failure traces show up in the logs. For rough monitoring, probe HTTPS or watch acme.json's mtime.
Middlewares: don't copy cross-cutting logic into every service
Security headers, compression, auth, rate limiting — make them reusable middleware. I prefer one generic header set in the file provider: HSTS, X-Content-Type-Options, Referrer-Policy, a modest CSP, then on the route:
- traefik.http.routers.app.middlewares=sec-headers@file
Every HTTPS route hangs off the same chain, so you don't copy headers into each compose and copy-paste a typo along with them. For websocket apps with mysterious disconnects, add a headers or timeout middleware for that app — don't start by changing global defaults.
api.dashboard: the route table isn't a view
- --api.dashboard=true
The dashboard shows every router and service — great for debugging, and just as great for attackers. Put BasicAuth / ForwardAuth on it, or bind it to the WireGuard subnet only; never leave it bare on the public internet. exposedbydefault=false governs business containers; the dashboard is Traefik's own entry and needs its own lockdown.
Internal DNS and the reality beyond labels
For upload-heavy services like Immich, point internal DNS at the internal IP so photos don't loop out to the public internet and back. Labels can only describe "how to route inside the container network" — they can't explain which path the client takes to get in. Split DNS, or host overrides in the router, is far more honest than making a phone app go through the public domain and back into your living-room switch. Upload traffic punishes you twice, in CDN billing and latency.
Give the network plane its own thought: Traefik and the business containers share one Docker network (the proxy one in the examples above), so service ports don't need to map to the host anymore. Letting only Traefik expose 80/443 collapses "what can be scanned" down to a single process. When onboarding a new service, join the network first, then write labels — reverse the order and you get "container is running but Traefik says backend unreachable."
To learn the concepts, wire up just two backends: a static page and a websocket app. Get the five threads — entrypoint, router, service, middleware, certresolver — running end to end, then roll labels out in bulk. My debugging mantra: curl -v first to see whether the cert and route hit, then Traefik's own access logs, and only then suspect the app. Once it's rolling, adding a service is often just a few labels — which is exactly the daily experience I want.