Caddy’s reverse_proxy directive covers everything from “send this domain to port 3000” to multi-backend load balancing with health checks — with automatic HTTPS in front. These are the patterns you’ll use most. (New to Caddy? Start with installing Caddy.)

One domain, one app

app.example.com {
	reverse_proxy 127.0.0.1:3000
}

Caddy terminates TLS, forwards the request, and sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host. The original Host header is passed through. WebSocket upgrades work automatically.

Bind your app to 127.0.0.1 so it’s only reachable through Caddy.

Several apps on one server

app.example.com {
	reverse_proxy 127.0.0.1:3000
}

api.example.com {
	reverse_proxy 127.0.0.1:8000
}

grafana.example.com {
	reverse_proxy 127.0.0.1:3001
}

Each site gets its own certificate.

Path-based routing

example.com {
	handle /api/* {
		reverse_proxy 127.0.0.1:8000
	}

	handle_path /admin/* {
		reverse_proxy 127.0.0.1:9000
	}

	handle {
		root * /var/www/frontend
		try_files {path} /index.html
		file_server
	}
}
  • handle keeps the path (/api/users → backend receives /api/users).
  • handle_path strips the matched prefix (/admin/settings → backend receives /settings).
  • The final handle serves a single-page app with fallback to index.html.

Load balancing

api.example.com {
	reverse_proxy 10.0.0.11:8000 10.0.0.12:8000 10.0.0.13:8000 {
		lb_policy least_conn
		lb_retries 2

		health_uri /health
		health_interval 10s
		health_timeout 3s

		fail_duration 30s
	}
}
  • Active health checks (health_uri) poll each backend and stop sending traffic to unhealthy ones.
  • Passive health checks (fail_duration) mark a backend down after failed requests.
  • Policies: random (default), round_robin, least_conn, ip_hash, cookie (sticky sessions), first (failover).

Headers

app.example.com {
	reverse_proxy 127.0.0.1:3000 {
		header_up X-Real-IP {remote_host}
		header_down -Server
	}
	header {
		Strict-Transport-Security "max-age=31536000"
		X-Content-Type-Options nosniff
		-X-Powered-By
	}
}

header_up modifies the request to the backend; header_down the response from it; header sets response headers for the client.

Backends over HTTPS

reverse_proxy https://10.0.0.20:8443 {
	transport http {
		tls_server_name internal.example.com
		# tls_insecure_skip_verify   # only for self-signed test backends
	}
}

Timeouts and large uploads

files.example.com {
	request_body {
		max_size 2GB
	}
	reverse_proxy 127.0.0.1:8080 {
		transport http {
			read_timeout 10m
			write_timeout 10m
		}
		flush_interval -1     # stream responses immediately (SSE, progress)
	}
}

Authentication and IP restrictions

admin.example.com {
	@outside not remote_ip 203.0.113.0/24 10.8.0.0/24
	respond @outside "Forbidden" 403

	basic_auth {
		alice $2a$14$…    # caddy hash-password
	}
	reverse_proxy 127.0.0.1:9000
}

For SSO, forward_auth integrates with Authelia or Authentik — see Authelia.

Behind Cloudflare or another proxy

Tell Caddy which proxies to trust for client IPs:

{
	servers {
		trusted_proxies static 173.245.48.0/20 103.21.244.0/22
	}
}

(List all of Cloudflare’s published ranges, or use a module that fetches them.)

Reusable snippets

(proxy_defaults) {
	encode zstd gzip
	header -Server
}

app.example.com {
	import proxy_defaults
	reverse_proxy 127.0.0.1:3000
}

Troubleshooting

  • 502 — the backend isn’t listening where you said; test with curl http://127.0.0.1:3000.
  • Redirect loops — the app forces HTTPS but doesn’t trust X-Forwarded-Proto; configure its proxy settings.
  • Certificate errors — DNS not pointing to the server, or ports 80/443 blocked; check journalctl -u caddy.

Related: Caddy with Docker, Nginx reverse proxy and HAProxy.

  • Caddy
  • Reverse Proxy
  • Web Server