Learning Caddy - Reverse Proxy Basics
Episode 6 of 31

Learning Caddy - Reverse Proxy Basics

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

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 reverse_proxy Directive

Basic Syntax

The simplest form forwards all requests to a single backend:

Basic reverse proxy
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.

Multiple Backends and Load Balancing

A single directive can accept several backends — Caddy distributes requests automatically:

Multiple backends
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.

Header Manipulation

Headers to Upstream (header_up)

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:

Modify upstream headers
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).

Headers to Downstream (header_down)

Response headers from the backend can be modified before returning to the client:

Modify downstream headers
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.

Common Reverse Proxy Patterns

Proxying to a Docker Container

When Caddy runs on the host and the app in a container, use the container name or IP:

Proxy to a container
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.

WebSocket Proxying

Caddy detects WebSocket upgrades automatically and proxies those connections with no special configuration:

Automatic WebSocket
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 Proxying

gRPC requires HTTP/2. Caddy handles this through the right transport:

Proxy gRPC with h2c
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.

Transport Options

HTTP Versions and Timeouts

The transport block controls how Caddy talks to the backend:

Transport options
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.

Keep-Alive

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.

Use Case: Exposing Modern Applications

With a reverse proxy, you can expose:

  • Node.js applications on random ports like 3000.
  • Python applications (Django, Flask) on port 8000.
  • Containers on an internal Docker network.
  • Microservices on different ports under a single domain.
  • API backends using REST or gRPC.

caddy reload --config Caddyfile is enough to apply changes without dropping active connections — one of the reasons Caddy is comfortable in operations.

Conclusion

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.
  • WebSockets are proxied automatically with no configuration.
  • gRPC needs 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.

Learning Caddy - Reverse Proxy Basics | Learning Caddy