Reverse proxies
The layer that terminates client connections and forwards them to backends. What looks like one product category is really five tools from three different eras answering different questions: HAProxy — the pure load balancer (one job, done to perfection); nginx — the web server that proxies (serve static + proxy dynamic under one config); Apache httpd — the venerable generalist (modules for everything, .htaccess, the pre-nginx incumbent); Caddy — the batteries-included modern one (automatic HTTPS by default, config ergonomics); Traefik — the cloud-native router (discovers backends from your orchestrator, no restarts).
The field (all captured)
- haproxy (OSS) (6.9k★, C, GPL-2.0) — TCP/HTTP load balancer since 2000; the reference for connection handling, health checks, and observability of the proxy layer itself. No TLS-automation story, no config discovery — deliberately out of scope. The kernel of most “high availability” stacks, fronting nginx/ingress layers rather than replacing them.
- nginx (OSS) (31.8k★, C, BSD-2) — web server + reverse proxy; event-driven architecture that broke Apache’s process-per-connection model in 2004. The default answer for “serve files, cache, TLS-terminate, and proxy to app upstreams” for two decades. This homelab’s
*.hedge is nginx-adjacent territory (homelab*.hproxy conventions). - httpd (OSS) (4.0k★, C, Apache-2.0) — Apache HTTP Server: MPM flexibility (prefork/event), dynamic config per directory (
.htaccess), module universe. Still the shared-hosting and legacy-app workhorse; the one whose model nginx rebelled against. - caddy (OSS) (76.2k★, Go, Apache-2.0) — HTTP/1-3 server whose killer feature is automatic HTTPS: ACME (Let’s Encrypt/ZeroSSL) issuance and renewal built-in, zero config. Caddyfile simplicity vs nginx’s ceremony. Extensible via Go plugins (xcaddy builds) — which is exactly the hook Uncloud uses (## Uncloud and standard Caddy (below)).
- traefik (OSS) (65.0k★, Go, MIT) — “The Cloud Native Application Proxy”: backends discovered from Docker/K8s labels, Let’s Encrypt automatic, config changes without restarts, built-in dashboard. The choice when your backends churn (containers) rather than when they’re static VMs.
The decision axes
- Automatic TLS: Caddy and Traefik native; nginx needs certbot sidecar/tooling; httpd similar (mod_md exists but less traveled); HAProxy out of philosophy (front it or back it with a TLS layer).
- Backend discovery: Traefik’s whole identity (labels → routes); everyone else is config-file-driven, though ecosystems script it.
- Raw TCP vs HTTP: HAProxy the specialist; nginx streams module; Traefik handles TCP/UDP; Caddy has layer-4 via plugin (caddy-l4).
- Ops culture: one static file, reload on change (nginx/haproxy/httpd) vs API-driven dynamic reconfig (Caddy admin API, Traefik providers).
- Footprint: all small in absolute terms; HAProxy famously lean; Traefik heaviest of the Go pair by feature surface.
Uncloud and standard Caddy
uncloud (OSS) (5.5k★, Go) ships Caddy as its built-in ingress: a per-machine global service, auto-generated Caddyfile from published ports (x-caddy compose extension for custom blocks), automatic Let’s Encrypt. The 0.21 release (the psviderski tweet) added clustered TLS certificate storage: Caddy instances share ACME tokens and certs through Uncloud’s distributed cluster store with leaderless locks, so any instance can answer a challenge behind a load balancer.
Does it work on standard Caddy? Not out of the box — and that’s by design. Grounded in the repo (pkg/client/caddy.go: CaddyImage = "caddy" — official Docker Hub image is the default) and the module README:
- Uncloud defaults to the stock
caddyimage; cluster storage requires thecaddy.storage.uncloudmodule, which stock Caddy lacks. The CLI error says it outright: certificates appear only when Caddy is “configured withstorage uncloudin the global options” via the module. - Two supported routes: the pre-built image
ghcr.io/unlabs-dev/caddy-uncloud:0.1.2(bundles the storage module + caddy-l4 + cloudflare DNS), deployed withuc caddy deploy --image ghcr.io/unlabs-dev/caddy-uncloud:0.1.2 --caddyfile Caddyfile; or build your own with xcaddy (--with github.com/unlabs-dev/caddy-uncloud). - The custom image contract is small: “must include curl and start Caddy with /etc/caddy/Caddyfile” (healthcheck hits the admin socket over curl).
- The Uncloud API unix socket is bind-mounted into the container (
/run/uncloud/api) — that’s the transport the module uses to reach the cluster store. No external dependency (no Redis/consul), at the cost of coupling Caddy to Uncloud’s daemon.
So: standard Caddy runs fine as Uncloud ingress with per-machine local cert storage (each instance issues its own); the moment you scale ingress across machines behind one LB/DNS name, you need the module image. caddy-uncloud (OSS) captured separately.
Deploy semantics: the proxy IS the deploy strategy
The zero-downtime compose pair (StatusDude war story + bex.co analysis, both captured 2026-10-03) settles which proxy feature actually matters for rolling deploys on a single box — and it’s not dashboards or discovery:
- Traefik failed the deploy (StatusDude, 4 hours, ripped out): two services with the same routing labels = “Service defined multiple times” flat refusal; scale-down race = routing-table lag sending traffic to dying containers (502s); the killer — retry middleware retries on the same dying backend, and redispatch-to-another-backend is the still-open traefik#2723. “Your proxy’s retry semantics are your deploy strategy’s failure semantics.”
- HAProxy won on one directive:
option redispatch— retries land on a different backend on conn-failure/empty-response/timeout/502/503/504. nginx’sproxy_next_upstreamhas the same shape (“Nginx would do too” — StatusDude’s own P.S.). - Three layered health mechanisms, none sufficient alone: per-request retry (ms), passive observation
observe layer7(catches erroring-under-traffic without probes), activeoption httpchk(catches silent death without traffic). - The recipe:
deploy.replicas: 2+, DNS-resolved backends, stop-one/start-one/--waitloop — ~60 lines HAProxy + a 10-line script, zero dropped requests in production at thousands of checks/min. - bex.co’s fleet-gap table (compose-native vs K8s+progressive delivery: cross-machine scheduling, readiness-gated maxSurge/maxUnavailable, metric-driven auto-rollback, canary %, failure domains) and the PaaS conclusion: borrow compose’s semantics, never fork the topology — one fleet-grade primitive that degrades gracefully to one node (the exact Uncloud design bet), compose bootstrap allowed only for the provisioner itself.
The same pattern at the cold-start extreme: KubeElasti (OSS) (280★, Go, MIT) scales idle services to zero and wakes them on first request — holding incoming traffic in a proxy queue while pods start, “so that no request is lost.” It’s the StatusDude insight inverted: there the proxy absorbs the death of backends during deploys (redispatch to a live one); here the proxy absorbs the absence of backends (queue until one exists). One-sentence rule covering both: the proxy owns every request from arrival to a ready backend — the gap between “traffic arrived” and “backend ready” is always the proxy’s problem. KubeElasti handles only 0↔1 scaling and hands 1↔n back to HPA/Keda — a deliberately narrow reconciliation scope (see Kubernetes Operators).
Sources: clippings/statusdude-com-blog-zero-downtime-docker-compose.md · clippings/bex-co-blog-2026-09-25-zero-downtime-docker-compose-blue-green-paas.md (also covers docker-rollout, Dokploy/Swarm, Coolify, Dokku ceilings)
Homelab read
Static set of services on known hosts → nginx or stock Caddy; containers churning → Traefik; need a dedicated TCP/edge LB → HAProxy fronting whatever. Uncloud’s model (WireGuard mesh + stock-Caddy-with-plugin ingress + compose) is the “missing middle” candidate for the doombox+Pi fleet — see Homelabs.
Related: Homelabs · Mail servers (direct-port vs proxied services) · Flux vs Argo CD (declarative infra kin) · sources: clippings/x-2105983356680683539-thread.md (psviderski 0.21 thread + GigoVich quote) · uncloud/caddy-uncloud repos