*Last updated: November 2026 — pin proxy image tags; ACME behavior depends on DNS and firewall.*
For: One public VPS, several Docker apps (Vaultwarden, Paperless, Immich, etc.), automatic HTTPS via Let’s Encrypt HTTP-01.
Do not use this page when: You need Cloudflare Tunnel only (no open 80/443), or a multi-node ingress mesh. Different design.
Guides on this site use Caddy 2 in Compose: publish only Caddy on 80/443/443/udp, apps stay on the internal network. Traefik is the main alternative when you want label-driven routing.
Priority
Prefer
Shortest config, few apps, readable Caddyfile
**Caddy**
Many stacks, routes defined on each service via labels
**Traefik**
Lowest “surprise” for beginners copying tutorials
**Caddy**
Dynamic add/remove services without editing a central file
**Traefik**
Minimal moving parts on a 1–2 GB VPS
**Caddy** (often lighter ops; measure both)
Topic
Caddy 2
Traefik (v2/v3)
Config style
Caddyfile (or JSON) — host → `reverse_proxy`
Static config + **Docker labels** on each app
ACME HTTP-01
Built-in; often zero extra flags
Built-in certificate resolvers (`certificatesResolvers`)
Where routes live
Usually one central Caddyfile
Scattered on services (`traefik.http.routers...`)
Docker integration
Reverse proxy by **service name** on Compose network
Provider watches Docker socket / labels
Needs Docker socket?
No
Yes for automatic discovery (security tradeoff)
Dashboard
Not required for basic reverse proxy
Optional API/dashboard (lock down if enabled)
UDP / HTTP3
Can publish `443/udp` for HTTP/3
Supported with correct entrypoints
Learning curve for one hostname
Very low
Higher (entrypoints, routers, services, middlewares)
Multi-host on one VPS
Multiple site blocks in one Caddyfile
Multiple routers / rules
Item
Practical note
Idle proxy RAM
Both are usually tens of MB idle; **your apps** dominate. Do not pick Traefik “for performance” on a 2 GB box — pick for **workflow**.
Failure domain
Caddy: bad Caddyfile breaks reload. Traefik: bad labels → route missing while other routes work.
Socket risk
Traefik + `/var/run/docker.sock` means a compromised Traefik config path can interact with Docker — treat Traefik like an admin component.
Compose tutorials
Most self-host app docs show either raw ports or nginx/Caddy snippets; Traefik needs label translation.
One Compose project or a dedicated proxy project on a shared external network.
vault.example.com {
reverse_proxy vaultwarden:80
}
docs.example.com {
reverse_proxy paperless:8000
}
photos.example.com {
reverse_proxy immich-server:2283
}
Publish only on Caddy:
ports:
- "80:80"
- "443:443"
- "443:443/udp"
Apps: no ports: to the host (only expose or internal listen).
Illustrative labels (pin Traefik image yourself):
services:
traefik:
image: traefik:v3.2
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
- --certificatesresolvers.le.acme.email=you@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
vaultwarden:
image: vaultwarden/server:1.37.5
labels:
- traefik.enable=true
- traefik.http.routers.vw.rule=Host(`vault.example.com`)
- traefik.http.routers.vw.entrypoints=websecure
- traefik.http.routers.vw.tls.certresolver=le
- traefik.http.services.vw.loadbalancer.server.port=80
Every new app needs matching labels + DNS A/AAAA records.
Check
Pass condition
DNS
Hostname A (and AAAA if used) → this VPS
Firewall
TCP 80 and 443 allowed inbound
Port owners
Only the proxy binds host 80/443 (`ss -tlnp \
grep -E ':80\
:443'`)
App URL env
`DOMAIN` / `PAPERLESS_URL` / public Immich URL use `https://` + same hostname
IPv6
If AAAA exists, proxy must listen on IPv6 or remove AAAA (see cert-fix page)
2–10 hostnames, one person maintains a Caddyfile in git
You refuse to mount Docker socket for a proxy
You want the same pattern as this site’s app guides
Many stacks owned by different compose projects; each stack ships its own labels
You add/remove services often and hate editing a central file
You already standardized on Traefik middlewares (auth, rate limit) across apps
Moving Caddy → Traefik (or reverse): change who terminates TLS and update app “public URL” env vars. Data volumes of apps do not move. Certificate storage is separate (caddy-data vs Traefik acme.json) — expect a new cert issuance on cutover (rate limits apply if you flap).
curl -I https://app.example.com returns the app, not connection refused
Certificate issuer is Let’s Encrypt (or your intended CA), not a browser warning
HTTP http:// redirects or upgrades as you configured
Stopping one app does not take down the proxy or other hostnames
docker compose logs on proxy shows successful obtain/renew, not challenge failures
Fix: Caddy unable to get certificate (DNS, ufw, IPv6)
Coolify vs Portainer vs plain Compose
Common Compose errors