CADDY β Caddy Web Server & Reverse Proxy
Last reviewed: 2026-09-17
Purpose: Using Caddy as a web server and reverse proxy β installation, Caddyfile syntax, automatic HTTPS, the JSON admin API, routing patterns, logging, and basic load balancing.
1. What Caddy Is
Caddy is an open-source HTTP/1.1, HTTP/2 and HTTP/3 server written in Go. Its two defining features are automatic HTTPS (it obtains and renews Let's Encrypt / ZeroSSL certificates without configuration) and a single static binary with no runtime dependencies. Configuration is either a compact Caddyfile or a full JSON document, and either form can be changed at runtime through the admin API.
| Feature | Notes |
|---|---|
| Automatic TLS | Certificate issue, renewal, OCSP stapling and HTTPβHTTPS redirects by default |
| Caddyfile | Human-friendly, terse config language for the common cases |
| JSON API | Canonical config model; native config for dynamic and programmatic setups |
| Admin API | localhost:2019 by default β read, patch or replace config while running |
| Reverse proxy | Upstream pools, health checks, header manipulation, WebSocket/gRPC support |
2. Installation
# Debian / Ubuntu β official repository
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddy
# Verify and manage
caddy version
sudo systemctl status caddy
sudo systemctl reload caddy # apply a changed Caddyfile without dropping connections
Validate a config before deploying it, or run Caddy in the foreground while testing:
caddy adapt --config /etc/caddy/Caddyfile # Caddyfile β JSON (syntax check)
caddy fmt --overwrite /etc/caddy/Caddyfile # normalise formatting
3. Caddyfile Basics
A Caddyfile is a set of site blocks: the first line is the address to serve, the braces contain directives.
# /etc/caddy/Caddyfile
example.com {
encode gzip zstd
root * /var/www/example.com
file_server
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
}
log {
output file /var/log/caddy/example.com.log
format json
}
}
| Token | Meaning |
|---|---|
example.com |
Site address β a hostname, hostname:port, :8080, or localhost |
http:// prefix |
Explicitly disables automatic HTTPS for that site |
root * |
Document root (* is the path matcher β all paths) |
file_server |
Serve static files from the root |
encode |
Compress responses (gzip, zstd) |
header |
Add, set or delete response headers |
log |
Access logging (JSON to file, stdout, or a network sink) |
For a local hostname that should not hit ACME, use http:// or the tls internal directive to issue a locally trusted certificate.
4. Reverse Proxy
The most common Caddy use is fronting local services with friendly hostnames or ports, and letting Caddy terminate TLS.
grafana.example.com {
reverse_proxy 127.0.0.1:3000
}
# Several routes in one block, matched by path prefix
app.example.com {
handle /api/* {
reverse_proxy 127.0.0.1:8000
}
handle /grafana/* {
reverse_proxy 127.0.0.1:3000
}
handle {
reverse_proxy 127.0.0.1:8080
}
}
# Port-based routes for a single machine running many self-hosted services
:5555 { reverse_proxy 127.0.0.1:5434 } # database
:2014 { reverse_proxy 127.0.0.1:11434 } # model server
:6001 { reverse_proxy 127.0.0.1:6000 } # dashboards
:9001 { reverse_proxy 127.0.0.1:9000 } # container admin UI
The most common sub-directives are header_up/header_down (rewriting headers and propagating client IPs), transport http { read_timeout ... } (raising timeouts for streaming endpoints) and health_uri for active upstream checks:
reverse_proxy 127.0.0.1:8000 {
header_up X-Real-IP {remote_host}
transport http {
read_timeout 60s
}
health_uri /health
}
WebSocket and gRPC traffic is proxied transparently.
5. Load Balancing
Multiple upstreams in one reverse_proxy are load-balanced (round-robin by default).
api.example.com {
reverse_proxy 10.0.0.11:8080 10.0.0.12:8080 10.0.0.13:8080 {
lb_policy least_conn # round_robin (default), random, first, ip_hash, uri_hash
lb_try_duration 5s # retry another upstream if one fails
health_uri /health
health_interval 10s
health_timeout 3s
}
}
ip_hash or cookie stickiness is needed when backends keep per-client session state.
6. Common Patterns
# Basic authentication with a bcrypt-hashed password
secure.example.com {
basic_auth {
admin $2a$14$<bcrypt-hash> # caddy hash-password --plaintext 'secret'
}
reverse_proxy 127.0.0.1:8000
}
# SPA / single-page app: serve files, fall back to index.html
spa.example.com {
root * /srv/spa
try_files {path} /index.html
file_server
}
# File browsing for an internal share
files.example.com {
root * /srv/share
file_server browse
}
Generate password hashes with caddy hash-password --plaintext 'secret' β never store plaintext credentials in a Caddyfile.
7. The JSON Config & Admin API
The Caddyfile is adapted into Caddy's native JSON config. Loading JSON directly is useful when configuration is generated by a tool, or when only part of a running config needs to change.
curl -s http://localhost:2019/config/ | jq . # read live config
curl -X POST http://localhost:2019/load \ # replace it wholesale
-H 'Content-Type: application/json' --data @config.json
caddy adapt --config Caddyfile --pretty # Caddyfile β JSON
A minimal JSON config that proxies one host and enables automatic HTTPS:
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [":443"],
"routes": [
{
"match": [{ "host": ["grafana.example.com"] }],
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [{ "dial": "127.0.0.1:3000" }]
}
],
"terminal": true
}
]
}
}
}
}
}
The admin endpoint listens on localhost:2019 by default and lets you patch rather than replace parts of the config (POST /config/apps/http/servers/srv0/routes). Restrict it to localhost, or disable it with admin off, in production β whoever reaches the admin API can rewrite routing.
8. Logging & Running as a Service
# Access logs to a file, rotation handled by logrotate
log {
output file /var/log/caddy/access.log {
roll_size 50MiB
roll_keep 5
}
format json
}
Reload safely after editing the config β Caddy validates the new config before switching, and a failed reload leaves the running configuration untouched:
9. Operational Notes
Certificates and keys live under $XDG_DATA_HOME/caddy (for the caddy system user, typically /var/lib/caddy/.local/share/caddy) β back that directory up, because losing it means re-issuing every certificate against ACME rate limits. Prefer hostnames with real DNS records so validation succeeds, use tls internal only for locally resolved names, and remember that only one process can bind :80 and :443 per host.