Reverse Proxy + Process Manager
Every self-hosted production app eventually needs two pieces in front of it: a reverse proxy (for TLS, gzip, routing, rate limits) and a process manager (for supervision, logs, restarts). The proxy choice is more contentious than the supervisor choice — and the two interact in ways that aren’t always obvious. This post walks through Caddy, nginx, and Traefik, the trade-offs, and how each pairs with a supervisor on the same box.
The Job of Each Layer
A reverse proxy answers from the public internet. It:
- Terminates TLS.
- Routes requests to upstream services by hostname or path.
- Adds gzip/zstd compression.
- Rate-limits, denies bad bots, sets security headers.
- Survives upstream failures with retries or graceful errors.
A process manager owns the upstream side. It:
- Keeps your app processes alive.
- Captures stdout/stderr.
- Restarts on crash.
- Stops cleanly on deploy.
They are complementary, not competing. The pairing decision is “which proxy fits my workflow,” not “do I need both.”
Caddy
The marketing line is true: Caddy ships with automatic Let’s Encrypt SSL out of the box. A complete reverse proxy for one app:
api.example.com {
reverse_proxy 127.0.0.1:3000
} Two lines. TLS cert is fetched and renewed automatically. HTTP/2 and HTTP/3 enabled by default.
Strengths:
- Zero-config TLS.
- Caddyfile is genuinely readable.
- Sensible defaults (security headers, modern TLS, compression).
- Built-in WebSocket and gRPC support — no special config.
- Single binary, no module compilation.
Weaknesses:
- Slower than nginx at very high RPS (still fast — usually doesn’t matter).
- Smaller community, fewer Stack Overflow answers for exotic cases.
- Plugin ecosystem requires recompiling Caddy with
xcaddy.
Best for: small-to-medium self-hosted sites where you want TLS to be a non-event.
Pairing with a process manager:
# oxfile.toml
[processes.api]
command = "node dist/server.js"
env = { PORT = "3000", HOSTNAME = "127.0.0.1" }
restart = "on-failure"
[processes.caddy]
command = "/usr/bin/caddy run --config /etc/caddy/Caddyfile"
restart = "on-failure" You can also run Caddy as a systemd service and only put your app in the supervisor — pick whichever feels less awkward. The advantage of putting both in oxfile.toml is one source of truth for “what’s running on this box.”
nginx
The default reverse proxy of the last decade. Battle-tested, ubiquitous, fast at any RPS you’re likely to throw at it.
A complete config for one app:
server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
} Plus a separate certbot invocation to get the certificate. And a renewal cron. And a reload hook. It’s all documented and works fine — it’s just more moving parts than Caddy.
Strengths:
- Fastest at high RPS.
- Best documentation by sheer volume.
- Most reverse proxy tutorials assume nginx.
- Tons of community modules, well-known config patterns.
- LuaJIT support via OpenResty for advanced use cases.
Weaknesses:
- TLS is your problem (certbot, renewal, reload).
- Verbose config files.
- WebSocket and gRPC need explicit upgrade headers.
- Modern protocols (HTTP/3) require extra steps.
Best for: high-traffic sites, teams that already know nginx, situations where you need fine control.
Full nginx config with SSL for a Node app is in the Node.js VPS setup post.
Traefik
The container-native option. Auto-discovers services from Docker, Kubernetes, or static config. Handles TLS automatically (like Caddy).
A static config for a non-container setup:
# traefik.yml
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
email: you@example.com
storage: /etc/traefik/acme.json
httpChallenge:
entryPoint: web
providers:
file:
filename: /etc/traefik/dynamic.yml # dynamic.yml
http:
routers:
api:
rule: "Host(`api.example.com`)"
service: api
tls:
certResolver: letsencrypt
services:
api:
loadBalancer:
servers:
- url: "http://127.0.0.1:3000" More config than Caddy. The win shows up when you have many services and label-based discovery makes sense — Docker Compose, K8s, Nomad.
Strengths:
- Shines in containerized environments.
- Excellent dashboard.
- First-class metrics (Prometheus endpoint built-in).
- Strong middleware ecosystem (auth, rate limiting, headers).
Weaknesses:
- Config-by-YAML can sprawl in non-container setups.
- Slower than nginx at high RPS.
- Overkill for one or two services on a single VPS.
Best for: Docker/K8s deployments with many services, dashboard-driven ops.
Picking One
| Use case | Pick |
|---|---|
| One Node app on a VPS, want TLS to “just work” | Caddy |
| High-traffic API, already know nginx | nginx |
| 10+ services in Docker Compose | Traefik |
| Want a single binary, minimal config | Caddy |
| Need OpenResty/Lua scripting | nginx |
| Need a built-in metrics dashboard | Traefik |
| WebSocket-heavy app | Caddy or nginx (with upgrade headers) |
| Self-host on a tiny VPS | Caddy (lowest memory) |
For most self-hosted projects in 2026, Caddy is the lowest-friction default. nginx is the right answer when you have specific needs Caddy can’t meet or when your team has 10 years of nginx muscle memory. Traefik is the right answer when your deployment is container-native.
How the Proxy and Supervisor Interact
Three patterns:
1. Proxy outside the supervisor.
systemd -> caddy
oxm -> api, worker, scheduled jobs Caddy runs as a system service. Your app processes run under a process manager. Simple, clear separation. Most common pattern.
2. Proxy inside the supervisor.
oxm -> caddy, api, worker, scheduled jobs Everything in one oxfile.toml. oxm status shows the proxy alongside your apps. One log destination. Good when you want “what’s running on this box” to be a single command.
3. Containerized everything.
Docker Compose or similar — see the Docker vs process manager post for the trade-offs.
Pattern 1 is the textbook approach. Pattern 2 is increasingly popular because it reduces cognitive load — there’s literally one place to look. Either works.
TLS Renewal: The Hidden Failure Mode
Caddy and Traefik handle renewals automatically. nginx via certbot needs the cron to actually run and the nginx config to actually reload. The classic outage:
- Certbot renews the cert at 3 AM.
- The reload hook fails silently because of a config typo from last month.
- 60 days later your cert expires.
- Your phone rings.
Mitigation: monitor your cert expiry independently of the renewal mechanism. A weekly cron that pings openssl s_client -connect api.example.com:443 </dev/null | openssl x509 -noout -dates and alerts if notAfter is within 14 days catches this.
With Caddy/Traefik this whole class of bug doesn’t exist.
Graceful Reload During Deploys
When you deploy a new version of your app:
- nginx and Caddy keep accepting requests for the old upstream while the new one starts.
- If you have two app instances behind the proxy, you can drain one, replace it, then drain the other — true zero downtime.
The zero-downtime deployment guide covers the blue-green pattern. It works the same way with any of these proxies — the only difference is the syntax for switching upstreams.
WebSocket-Specific Notes
If your app uses WebSockets, three rules:
- Caddy: works out of the box. No special config.
- nginx: needs explicit
UpgradeandConnectionheaders, plus a longproxy_read_timeout. - Traefik: works out of the box.
The full WebSocket scaling discussion is in WebSocket server scaling on a single VPS.
Bottom Line
There’s no universal winner. Caddy is the right default for new self-hosted projects in 2026. nginx is the right answer for high-traffic or nginx-fluent teams. Traefik is the right answer for container-native deployments.
For the supervisor layer, Oxmgr sits underneath any of them — managing your app processes, your worker queues, your scheduled tasks, and (optionally) the proxy itself from one config file.