3 min read #devops
On this page

Caddy Operations Manual

Caddy is a web server and reverse proxy written in Go. Its core selling points are automatic TLS (ACME + Let's Encrypt zero-configuration) and a concise Caddyfile configuration syntax. The configuration file location depends on the installation method: for package manager installations, it is typically at /etc/caddy/Caddyfile, while for manual deployments, it is at ./Caddyfile.

Configuration

Static File Server

example.com {
    root * /var/www/html                 # * matches all paths
    file_server                          # Static file serving (automatic directory listing)
    encode gzip zstd                     # Response compression
}

Reverse Proxy

grafana.example.com {
    reverse_proxy 127.0.0.1:3000 {
        # Headers passed to the backend (backend logs can see the real client)
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
        header_up Host {upstream_hostport}
    }
}

Multiple Backends (Load Balancing)

api.example.com {
    reverse_proxy backend-1:8080 backend-2:8080 backend-3:8080 {
        lb_policy least_conn               # round_robin / least_conn / random
        health_uri /health                  # Health check endpoint
        health_interval 30s
    }
}

mTLS (Mutual TLS Authentication)

otlp.example.com {
    tls /etc/caddy/certs/server.crt /etc/caddy/certs/server.key {
        client_auth {
            mode require_and_verify
            trusted_ca_cert_file /etc/caddy/certs/ca.crt     # Trusted CA
            trusted_leaf_cert_file /etc/caddy/certs/revoked.caddy  # Revocation list
        }
    }
    # Only allow specific paths, return 403 for the rest
    @otlp path /api/v1/otlp/*
    reverse_proxy @otlp 127.0.0.1:4318
    respond 403
}

Reloading and Hot Updates

# Validate configuration syntax (verify before reloading after changing Caddyfile)
caddy validate --config /etc/caddy/Caddyfile
# → Outputs "Valid configuration" or error details

# Hot reload (does not interrupt existing connections; new connections use the new configuration)
systemctl reload caddy
# Or caddy reload --config /etc/caddy/Caddyfile

# View the final generated JSON configuration (Caddy internally translates Caddyfile to JSON)
caddy adapt --config /etc/caddy/Caddyfile

# View the currently loaded configuration
caddy fmt --overwrite /etc/caddy/Caddyfile   # Format Caddyfile

Certificate Management

Caddy automatically manages Let's Encrypt certificates by default—issuing them via ACME HTTP-01 or TLS-ALPN-01 challenges on first startup, and automatically renewing them before expiration.

# View managed certificates
caddy list-certificates

# Data directory (certificates + OCSP staple + account key):
# /var/lib/caddy/.local/share/caddy/

# Use static certificates (do not auto-issue LE) — specify file paths in tls:
tls /etc/caddy/certs/example.crt /etc/caddy/certs/example.key

# Multi-domain SAN certificate
example.com, *.example.com {
    tls /etc/caddy/certs/wildcard.crt /etc/caddy/certs/wildcard.key
    ...
}

Logging

# Default JSON format output to stderr (collected by systemd journal)
journalctl -u caddy -f

# Or configure Caddyfile to write to a file:
# log {
#     output file /var/log/caddy/access.log {
#         roll_size 100mb
#         roll_keep 5
#     }
#     format json
# }

Troubleshooting

# Startup failure
caddy validate --config /etc/caddy/Caddyfile   # Check syntax first
journalctl -u caddy --since "5 min ago" --no-pager | tail -50

# TLS certificate errors
curl -vI https://example.com 2>&1 | grep -E 'SSL|cert|verify'
# → Certificate expired / domain mismatch / ACME challenge failed (check journalctl)

# 502 Bad Gateway (backend unreachable)
# → Can Caddy connect to the upstream?
curl -I http://127.0.0.1:3000
# → Check backend health: systemctl status <backend>

# HTTP/3 not working
# → Caddy enables h3 by default, but requires:
#   - UDP port 443 allowed by firewall
#   - Caddy's internal port equals external port (otherwise alt-svc port is incorrect)