Proxy applications
Proxy a local application
Place Caddy in front of one loopback application and verify the client and upstream as separate connections.
Most Caddy deployments do one thing: sit in front of an application. Your Node or Go or Python app listens on a local port. Caddy owns ports 80 and 443. The piece connecting them is a reverse proxy.
Why not expose the app directly? Because Caddy brings HTTPS, compression, access logs, and configuration reloads without downtime. Your framework either lacks those or does them worse. With Caddy in front, the app gets to focus on being an app.
Start an upstream
We need something to proxy to. Python’s built-in server is perfect as a stand-in:
python3 -m http.server 3000 --bind 127.0.0.1
Now the Caddyfile:
:8080 {
reverse_proxy 127.0.0.1:3000
}
The app behind the proxy is called the upstream. Here’s what happens for each request. The client connects to Caddy. Caddy opens a second connection to 127.0.0.1:3000, forwards the request, and relays the response back. Two connections, two log entries, one request.
Compare both paths
Let’s request the same page directly and through Caddy:
curl -i http://127.0.0.1:3000/
curl -i http://127.0.0.1:8080/
Same body both times. Now look at the Python terminal. It logged two requests, one from curl and one from Caddy. From the app’s point of view, Caddy is just another client.
Check the bind address
The most important detail in this setup is where the app listens. It’s bound to 127.0.0.1, not to all interfaces.
If the app also listens publicly, anyone can skip Caddy, and with it your TLS, your logs, and any authentication you add later. Verify it:
ss -lnt | grep 3000
You want 127.0.0.1:3000 in that output. If you see *:3000 or 0.0.0.0:3000, fix the app’s bind address before going any further. Every framework has an option for it.
Break it on purpose
Stop the Python process and curl through Caddy again. You get 502 Bad Gateway.
Now read the runtime log, in journalctl -u caddy or in your foreground terminal. It shows a dial error with the upstream address, something like dial tcp 127.0.0.1:3000: connect: connection refused.
Remember this signature. A 502 from Caddy almost always means the upstream didn’t answer. When you see one, check the app first, not the proxy. Restart the Python server and the 502 disappears without touching Caddy at all.
Lesson completed