This episode introduces the reverse proxy: the reverse_proxy directive, header manipulation to upstream and to the client, proxy patterns for localhost and containers, WebSocket and gRPC proxying, plus transport options like h2c and timeouts.

A reverse proxy is one of the main reasons people use Caddy in production. Instead of your application (Node.js, Django, Flask, or a container) being exposed directly to the internet on a random port, Caddy stands in front, handles TLS and domains, and forwards requests to the application behind it.
Episode 6 introduces the reverse_proxy directive from the basics: syntax, backend URLs, header manipulation, common patterns, and transport options. This is the foundation we'll expand into load balancing (episode 14), sticky sessions (episode 15), and failover (episode 16).
After this episode, you'll be able to expose any application to the internet safely and quickly in just a few lines of Caddyfile.
The simplest form forwards all requests to a single backend:
app.example.com {
reverse_proxy localhost:8080
}Now every request to app.example.com is forwarded to the application at localhost:8080. The backend URL can be localhost:port, 127.0.0.1:port, a hostname, or even http://backend:3000.
A single directive can accept several backends — Caddy distributes requests automatically:
app.example.com {
reverse_proxy localhost:8080 localhost:8081 localhost:8082
}By default it uses the round_robin policy. We'll dissect all the load balancing policies in episode 14.
Sometimes an application needs information about the real client — not Caddy's IP. Caddy adds X-Forwarded-* headers by default, but you can control the headers sent upstream:
app.example.com {
reverse_proxy localhost:8080 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up Host {host}
}
}header_up X-Real-IP {remote_host} tells the application the client's real IP address. The name header_up follows the direction: headers sent to the upstream (backend).
Response headers from the backend can be modified before returning to the client:
app.example.com {
reverse_proxy localhost:8080 {
header_down -Server
header_down X-App-Version v2.4
}
}header_down -Server removes the Server header leaking from your application; header_down X-App-Version v2.4 adds a custom header. This is useful for removing sensitive backend information — we'll continue this in episode 20.
When Caddy runs on the host and the app in a container, use the container name or IP:
api.example.com {
reverse_proxy web:3000
}On the same Docker network, the name web resolves to the container's IP address. If Caddy and the app are on different networks, use host.docker.internal:port or the container's IP address. Full details in episode 27.
Caddy detects WebSocket upgrades automatically and proxies those connections with no special configuration:
chat.example.com {
reverse_proxy localhost:9000
}Requests with the Upgrade: websocket header are forwarded as upgraded connections. No extra options needed — Caddy handles this natively. Episode 25 covers WebSocket in depth.
gRPC requires HTTP/2. Caddy handles this through the right transport:
grpc.example.com {
reverse_proxy localhost:50051 {
transport http {
versions h2c 2
}
}
}transport http { versions h2c 2 } forces the backend connection to use HTTP/2 cleartext (h2c) — the protocol gRPC needs. gRPC details are in episode 26.
The transport block controls how Caddy talks to the backend:
app.example.com {
reverse_proxy localhost:8080 {
transport http {
versions h1 h2
read_timeout 30s
write_timeout 30s
dial_timeout 10s
}
}
}versions h1 h2 — the backend can be served with HTTP/1.1 or HTTP/2.dial_timeout — the time limit for opening a connection to the backend.read_timeout and write_timeout — read and write time limits.Timeouts that are too short cut off slow requests; too long holds stuck connections. Start at 30 seconds and adjust to your application's needs.
Caddy keeps connections to the backend alive (keep-alive) by default. This avoids the cost of creating a new connection for every request — important for high-traffic applications.
With a reverse proxy, you can expose:
caddy reload --config Caddyfile is enough to apply changes without dropping active connections — one of the reasons Caddy is comfortable in operations.
Episode 6 introduced the reverse proxy: reverse_proxy with one or many backends, header manipulation with header_up and header_down, proxy patterns for localhost and containers, automatic WebSocket handling, gRPC with h2c, and transport options including HTTP versions and timeouts.
Key takeaways:
reverse_proxy localhost:port is enough for simple applications.X-Forwarded-* headers are sent to upstream automatically.header_up for headers to the backend, header_down for headers to the client.transport http { versions h2c 2 }.caddy reload applies changes without downtime.In the next episode, episode 7, we dive into Caddy's main magic: automatic HTTPS — the ACME protocol, Let's Encrypt integration, domain validation, automatic issuance and renewal, certificate storage, and the HTTP-01, TLS-ALPN-01, and DNS-01 challenge types.