Skip to content

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:

caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
journalctl -u caddy -f

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.