Reverse Proxy Configuration #
Providing a robust bridge between external traffic and backend applications is the core of professional web server management. Caddy’s reverse_proxy directive is designed to simplify this architecture by combining syntax simplicity with enterprise-grade advanced features. We’ll learn how to configure Caddy to distribute traffic intelligently, monitor server health automatically, and handle disaster recovery (failover) without sacrificing performance stability.
Single Upstream Configuration #
The most basic reverse proxy pattern is routing all traffic from one public domain to a single backend application running on a local port.
# Basic configuration example
example.com {
reverse_proxy localhost:3000
}
With just this one-line configuration, Caddy already enables the following mechanisms automatically behind the scenes:
- Connection Pooling: Maintains TCP connections open to the
:3000backend to minimize handshake latency on subsequent requests. - Header Forwarding: Automatically inserts
X-Forwarded-For,X-Forwarded-Proto, andX-Forwarded-Hostheaders so the backend server knows the client’s real network identity. - Default Timeouts: Applies safe wait limits (timeouts) so the server doesn’t hang when the backend is unresponsive.
If your application server runs on a separate physical server on the local network, just replace localhost with that server’s private IP address:
# Example proxy to a private IP
api.example.com {
reverse_proxy 10.0.0.15:8080
}
Multiple Upstream Configuration (Load Balancing) #
As your application grows, relying on a single backend server is a Single Point of Failure. You can register several backend servers at once (multiple upstreams) so Caddy acts as a load balancer.
# Load balancing to 3 backend instances
app.example.com {
reverse_proxy backend-1:3000 backend-2:3000 backend-3:3000
}
Or you can list those backends using a structured configuration block with the to directive to make your Caddyfile look cleaner:
# Structured writing format
app.example.com {
reverse_proxy {
to backend-1:3000
to backend-2:3000
to backend-3:3000
lb_policy round_robin
}
}
Load Balancing Policies #
Caddy provides various algorithm policies to determine how incoming requests are distributed across backend servers:
round_robin(Default): Distributes traffic alternately and sequentially from the first to the last backend (1, 2, 3, 1, 2, 3…). This algorithm is ideal when all backends have identical hardware specs.least_conn: Caddy monitors the number of active requests each backend is processing and routes new requests to the backend with the smallest current workload. This is the best option for requests with variable processing durations.random: Picks a backend randomly for each incoming request.random_choose <n>: First picks<n>backends randomly (e.g., 2 backends), then from those two chosen backends, Caddy selects the one with the fewest active connections. This technique balances random decision speed with distribution fairness.first: Caddy always sends traffic to the first registered backend. Subsequent backends are only used if the previous one is declared unhealthy. Perfect for primary-backup scenarios.ip_hash: Caddy computes a hash of the client’s IP address and uses it to determine the backend. This ensures the same client always connects to the same backend (IP-based sticky session).uri_hash: Caddy maps requests by URI path. Requests to the same URL are always processed by the same backend, very useful for improving backend local caching efficiency.cookie: Caddy inserts a tracking cookie into the client’s browser on the first request and uses it to keep subsequent client sessions bound (sticky session) to the same backend instance.
# Example sticky session using a cookie
app.example.com {
reverse_proxy backend-1:3000 backend-2:3000 {
lb_policy cookie {
name session_id
secret "production-secret-key-must-be-long"
}
}
}
Health Checks #
To ensure users don’t get error pages when one backend dies, Caddy can perform periodic health monitoring. Backends detected as problematic are immediately removed from the traffic distribution list.
flowchart TD
Caddy[Caddy Proxy] -->|Normal Access| B1[Backend 1: Healthy]
Caddy -.->|Connection Terminated| B2[Backend 2: Dead]
subgraph Healthcheck_Engine[Healthcheck Engine]
HC[Active Poller] -.->|Periodic /health ping every 10s| B2
HC -->|Backend 2 Fails 3x| MarkUnhealthy[Mark Unhealthy]
end
style B1 stroke:#43a047,stroke-width:2px
style B2 stroke:#e53935,stroke-dasharray: 5,51. Active Health Checks #
In this mechanism, Caddy proactively sends HTTP requests periodically to a special URL path provided by the backend to check its health directly.
# Active health check configuration
app.example.com {
reverse_proxy backend-1:3000 backend-2:3000 {
health_uri /healthz
health_interval 10s
health_timeout 5s
health_status 200
health_headers {
X-Checker "Caddy-Monitor"
}
}
}
health_uri: The backend endpoint to contact (usually returning the health status of the database and the backend’s internal services).health_interval: The pause between checks (in the example above, every 10 seconds).health_timeout: The response wait limit. If the backend doesn’t answer within 5 seconds, the check is considered failed.health_status: The expected HTTP status to declare the backend healthy (usually200).
2. Passive Health Checks #
Unlike active checks, passive health checks work silently by monitoring failures from real user request transactions.
# Passive health check configuration
app.example.com {
reverse_proxy backend-1:3000 backend-2:3000 {
health_fails 3
fail_duration 30s
max_fails 3
}
}
If Caddy detects connection failures or transport-level errors 3 times (health_fails) in a row within a 30-second window (fail_duration), that backend is immediately marked unhealthy and temporarily disabled from receiving new traffic.
Failure Handling: Retry and Circuit Breaker #
When a backend experiences a brief disruption (network hiccup), you want Caddy to try resending the request to a backup backend before giving up and returning an error page to the user.
# Retry duration configuration
app.example.com {
reverse_proxy backend-1:3000 backend-2:3000 {
lb_try_duration 5s
lb_try_interval 250ms
}
}
lb_try_duration: Tells Caddy to keep trying to shift the request to another healthy backend for up to 5 seconds before finally giving up and returning a502 Bad Gatewayerror status.lb_try_interval: The pause between each connection shift attempt (in the example above, 250 milliseconds).
The Circuit Breaker Cycle #
By combining health_fails, fail_duration, and lb_try_duration, Caddy applies an automatic Circuit Breaker pattern to protect overwhelmed backends from being continuously bombarded by new requests:
stateDiagram-v2
[*] --> Closed: Backend Running Normally
Closed --> Open: Consecutive failures > max_fails (Circuit Breaker Active)
Note right of Open: Traffic is routed to other backends
Open --> HalfOpen: fail_duration time expires
HalfOpen --> Closed: Test request succeeds (Backend recovered)
HalfOpen --> Open: Test request fails againWebSocket Proxying and Data Streaming #
One of Caddy’s big advantages is its automatic handling of persistent connection protocols like WebSocket and Server-Sent Events (SSE) without requiring any special declarations.
# Automatic WebSocket reverse proxy configuration
chat.example.com {
# Caddy detects the 'Upgrade: websocket' header automatically
reverse_proxy localhost:8080
}
If your chat application uses WebSocket, Caddy automatically detects the Upgrade: websocket and Connection: Upgrade handshake headers sent by the client, then upgrades that TCP connection into a persistent full-duplex two-way communication channel.
Caching Buffering vs Streaming (SSE) #
For applications that send responses gradually and continuously (streaming) like Server-Sent Events (SSE) APIs or dynamic video playback, Caddy by default holds the response in an in-memory buffer first before sending it to the client for data transfer efficiency.
However, this behavior breaks real-time communication on SSE services. You must instruct Caddy to immediately send each data chunk without delay using the flush_interval -1 parameter:
# Securing real-time streaming connections
stream.example.com {
reverse_proxy localhost:8000 {
# Flush data immediately to the client without buffering
flush_interval -1
}
}
Path Rewriting #
In microservice architectures, backend servers are often designed to serve requests from a base URL path (root path /), while Caddy separates them by public subdirectory name prefixes (like /api/ or /app/).
flowchart TD
Client["Client sends: GET /api/v1/users"] -->|"Caddy strips '/api'"| Backend["Backend receives: GET /v1/users"]If you forward the request directly, your backend returns a 404 Not Found error because it doesn’t recognize the /api path prefix. You must strip that path prefix using a combination of the handle block and the uri strip_prefix directive before sending it to the backend:
# Path prefix stripping example
example.com {
# Route all requests starting with /api to the API backend
handle /api/* {
uri strip_prefix /api
reverse_proxy localhost:8080
}
# All other requests are served by the frontend web server
handle {
root * /var/www/frontend/dist
file_server
}
}
Dynamic Upstreams #
In modern cloud computing infrastructure like Docker Swarm, Kubernetes, or HashiCorp Consul, backend server IP addresses change dynamically every time a redeploy or autoscaling process happens. Writing static IP addresses in the Caddyfile makes maintenance difficult.
Caddy solves this by providing the dynamic subdirective, which enables dynamic backend address discovery (service discovery) through real-time DNS queries.
# Using dynamic DNS resolution
app.example.com {
reverse_proxy {
dynamic a {
# Query A/AAAA DNS records dynamically
name backend-service.local
port 3000
refresh 30s
resolvers 10.0.0.2 1.1.1.1
}
}
}
dynamic a: Tells Caddy to periodically query theAorAAAArecord type of thebackend-service.localdomain.refresh: Controls how often Caddy re-queries DNS to update the backend IP list (in this example, every 30 seconds).resolvers: The IP addresses of your internal network DNS servers holding the service name map.
Complete Production Configuration Pattern #
Here’s a Caddyfile template for production environments combining all the best techniques: structured logging, dynamic response compression, load balancing with sticky sessions, connection failure protection, active health monitoring, and custom maintenance pages.
# Global option to enable debug logging (if needed)
{
email [email protected]
}
# Main site configuration block
app.example.com {
# Enable modern data compression
encode gzip zstd
# Structured access logging in JSON format
log {
output file /var/log/caddy/app_access.log {
roll_size 100mb
roll_keep 10
roll_keep_for 720h
}
format json
}
# Reverse proxy to the main backend server cluster
reverse_proxy backend-node-1:3000 backend-node-2:3000 backend-node-3:3000 {
# Load distribution algorithm
lb_policy least_conn
# Connection transition failure handling
lb_try_duration 5s
lb_try_interval 200ms
# Active health monitoring to the backend endpoint
health_uri /healthz
health_interval 10s
health_timeout 3s
health_status 200
# Additional passive health monitoring
health_fails 3
fail_duration 30s
# Request header manipulation for backend security
header_up Host {upstream_hostport}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
# HTTP transport layer configuration to the backend
transport http {
dial_timeout 3s
response_header_timeout 15s
}
}
# Catch 502/503 errors if all backends are down and serve a maintenance page
handle_errors {
@service_down expression {err.status_code} in [502, 503, 504]
handle @service_down {
header Content-Type "text/html; charset=utf-8"
respond <<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server Under Maintenance</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; text-align: center; padding: 150px; background-color: #f7f9fa; color: #333; }
h1 { font-size: 40px; margin-bottom: 10px; color: #e53935; }
p { font-size: 20px; color: #666; }
</style>
</head>
<body>
<h1>We're Sorry</h1>
<p>Our service is undergoing scheduled maintenance or the backend capacity is full. Please try again in a moment.</p>
</body>
</html>
HTML 503
}
}
}
Summary #
- Basic Syntax: The
reverse_proxydirective simplifies backend application gateway configuration with built-in connection pool management.- Load Balancing: Algorithm policies like
least_connare highly recommended for real workloads because they distribute traffic based on dynamic backend busyness.- Health Checks: Caddy supports proactive health monitoring (active checks via dedicated endpoint pings) and reactive observation (passive checks via real error transaction analysis) to dynamically remove problematic backends.
- Failover & Recovery: By setting
lb_try_duration, you give Caddy time tolerance to transparently shift requests to backup servers before returning an error status code to visitors.- WebSocket & Streaming: WebSocket connections are handled transparently without manual configuration, while SSE or streaming file transmission requires adding the
flush_interval -1parameter to prevent data buffer delays.