Skip to content

Caddy Cheat Sheet

Overview

Caddy is an open-source web server written in Go that makes HTTPS the default rather than an afterthought. The defining feature of Caddy is automatic TLS certificate management: it obtains, renews, and configures certificates from Let’s Encrypt and ZeroSSL without any manual intervention. When you define a domain name in a Caddyfile, Caddy automatically handles the ACME challenge, certificate issuance, and renewal, redirects HTTP to HTTPS, and even negotiates HTTPS upgrade headers — all out of the box.

The primary configuration format is the Caddyfile, which uses a clean, human-readable syntax designed to express common web server tasks without the verbosity of nginx or Apache configuration files. A full TLS-enabled reverse proxy to a backend application can be expressed in four lines. For programmatic or dynamic configuration, Caddy also exposes a full RESTful JSON API that allows live configuration changes without restarts or signal handling.

Caddy supports all modern web standards: HTTP/1.1, HTTP/2, and HTTP/3 (QUIC) out of the box. It handles WebSocket proxying, gRPC proxying, static file serving, URL rewrites, header manipulation, authentication middleware, rate limiting, and more through its modular plugin ecosystem. Caddy is widely used as a reverse proxy in front of applications like Gitea, Nextcloud, Vaultwarden, and Grafana, particularly in self-hosted setups where zero-touch TLS management is valuable.

Installation

Linux (Official Binary)

# Download from GitHub releases
curl -OL https://github.com/caddyserver/caddy/releases/latest/download/caddy_linux_amd64.tar.gz
tar -xzf caddy_linux_amd64.tar.gz
sudo mv caddy /usr/local/bin/
sudo chmod +x /usr/local/bin/caddy
caddy version

Ubuntu/Debian (Repository)

# Add Caddy repository
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
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 enable service
sudo systemctl enable --now caddy
caddy version

RHEL/CentOS/Fedora

# Install via COPR (Fedora/RHEL)
sudo dnf install -y 'dnf-command(copr)'
sudo dnf copr enable @caddy/caddy
sudo dnf install -y caddy

# Start service
sudo systemctl enable --now caddy

macOS

# Homebrew
brew install caddy

# Start as service
brew services start caddy

# Or run directly
caddy run --config /usr/local/etc/caddy/Caddyfile

Docker

# Simple Caddy container
docker run -d \
  --name caddy \
  -p 80:80 -p 443:443 -p 443:443/udp \
  -v $PWD/Caddyfile:/etc/caddy/Caddyfile \
  -v caddy_data:/data \
  -v caddy_config:/config \
  caddy:latest

# Docker Compose
cat > docker-compose.yml << 'EOF'
version: "3.9"
services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
      - ./www:/var/www/html:ro
    environment:
      - ACME_AGREE=true

volumes:
  caddy_data:
  caddy_config:
EOF

Configuration

Basic Caddyfile Structure

# Global options block (optional)
{
    email your@email.com
    admin localhost:2019
    debug
}

# Site block — domain triggers automatic HTTPS
example.com {
    # Serve static files
    root * /var/www/html
    file_server

    # Access log
    log {
        output file /var/log/caddy/access.log
    }
}

Caddyfile Syntax Basics

# Address formats
example.com            # Port 443 with auto HTTPS
:8080                  # Any host on port 8080
localhost              # Local with auto TLS via internal CA
http://example.com     # Force HTTP only
0.0.0.0               # All interfaces, port 80

# Multiple sites in one block
example.com, www.example.com {
    respond "Hello!"
}

# Environment variable substitution
{$MY_DOMAIN} {
    respond "Hello from {$MY_DOMAIN}"
}

Environment Variable Configuration

# Create /etc/caddy/Caddyfile using env vars
echo 'DOMAIN=example.com' > /etc/caddy/caddy.env

# Reference in Caddyfile
{$DOMAIN} {
    root * /var/www/html
    file_server
}

Core Commands

CommandDescription
caddy runRun Caddy in the foreground
caddy run --config CaddyfileRun with specific config file
caddy startStart Caddy as a background daemon
caddy stopStop the running Caddy daemon
caddy reloadReload config without downtime
caddy validate --config CaddyfileValidate a Caddyfile for syntax errors
caddy adapt --config CaddyfileConvert Caddyfile to JSON
caddy fmt CaddyfileFormat a Caddyfile
caddy fmt --overwrite CaddyfileFormat and overwrite in-place
caddy versionShow version information
caddy list-modulesList all available modules
caddy build-infoShow build info and module list
caddy environPrint environment info
caddy trustInstall Caddy’s local CA into system trust store
caddy untrustRemove Caddy’s local CA from trust store
caddy upgradeUpgrade to latest release
caddy add-package PLUGINAdd a plugin package

Advanced Usage

Reverse Proxy Patterns

# Simple reverse proxy
example.com {
    reverse_proxy localhost:3000
}

# Proxy with path prefix stripping
example.com {
    handle /api/* {
        uri strip_prefix /api
        reverse_proxy localhost:5000
    }
    handle {
        reverse_proxy localhost:3000
    }
}

# Proxy to Docker service by name
app.example.com {
    reverse_proxy app:8080
}

# WebSocket-compatible proxy (automatic in Caddy)
ws.example.com {
    reverse_proxy localhost:8000
}

# gRPC proxy
grpc.example.com {
    reverse_proxy h2c://localhost:50051
}

Load Balancing

example.com {
    reverse_proxy {
        # Multiple backends
        to backend1:8080 backend2:8080 backend3:8080

        # Load balancing policies
        lb_policy round_robin          # default
        # lb_policy least_conn
        # lb_policy ip_hash
        # lb_policy random
        # lb_policy first

        # Health checks
        health_uri /health
        health_interval 10s
        health_timeout 5s
        health_status 200

        # Retry on failure
        fail_duration 30s
        max_fails 3
    }
}

TLS Configuration

example.com {
    tls your@email.com   # ACME with this contact email

    tls {
        # Use specific ACME CA
        ca https://acme-v02.api.letsencrypt.org/directory

        # Staging CA for testing
        # ca https://acme-staging-v02.api.letsencrypt.org/directory

        # Use existing cert files
        # load /path/to/cert.pem /path/to/key.pem

        # Minimum TLS version
        protocols tls1.2 tls1.3

        # Allowed cipher suites
        ciphers TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
    }

    reverse_proxy localhost:8080
}

# Wildcard certificate
*.example.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }
    reverse_proxy localhost:8080
}

Authentication and Security

example.com {
    # Basic authentication
    basicauth /admin/* {
        # Use: caddy hash-password to generate hashes
        alice $2a$14$hkXoH.5dU2e9BahjeSEJAe...
    }

    # Security headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server
    }

    # Rate limiting (requires rate_limit plugin)
    rate_limit {
        zone dynamic_zone {
            key {remote_host}
            events 100
            window 1m
        }
    }

    reverse_proxy localhost:3000
}

URL Rewrites and Redirects

example.com {
    # Redirect www to apex
    @www host www.example.com
    redir @www https://example.com{uri} permanent

    # Rewrite internally (path change, no redirect)
    rewrite /old-path /new-path

    # Regex rewrite
    @legacy path_regexp ^/blog/(\d+)/(.+)$
    rewrite @legacy /posts/{re.0.1}-{re.0.2}

    # Redirect with status
    redir /downloads/* https://files.example.com{path} 302

    file_server
}

Caddy JSON API

# Get current running configuration
curl http://localhost:2019/config/

# Reload config from file via API
curl -X POST http://localhost:2019/load \
  -H "Content-Type: text/caddyfile" \
  --data-binary @Caddyfile

# Add a new site via API
curl -X POST http://localhost:2019/config/apps/http/servers/myserver \
  -H "Content-Type: application/json" \
  -d '{"listen": [":8080"], "routes": [...]}'

# Stop Caddy via API
curl -X POST http://localhost:2019/stop

Static File Server Options

files.example.com {
    root * /var/www/files

    # Enable directory browsing
    file_server browse

    # Download files as attachments
    # file_server {
    #     download
    # }

    # Custom index files
    file_server {
        index index.html index.htm
        hide .git .env
    }
}

Common Workflows

Self-Hosted App with Auto-HTTPS

# /etc/caddy/Caddyfile

# Global: contact email for Let's Encrypt
{
    email admin@example.com
}

# Main app — automatic HTTPS
app.example.com {
    reverse_proxy localhost:3000

    # Compress responses
    encode gzip zstd

    # Log requests
    log {
        output file /var/log/caddy/app.log
        format json
    }

    # Security headers
    header {
        X-Frame-Options DENY
        X-Content-Type-Options nosniff
        Strict-Transport-Security "max-age=31536000;"
        -Server
    }
}

# Redirect HTTP to HTTPS (automatic in Caddy)
# and www to apex
www.example.com {
    redir https://example.com{uri} permanent
}
# Validate and reload
caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
# Or
caddy reload --config /etc/caddy/Caddyfile

Multi-Application Reverse Proxy

{
    email ops@example.com
}

# Application 1
app1.example.com {
    reverse_proxy localhost:3001
}

# Application 2 with path routing
app2.example.com {
    handle /api/* {
        reverse_proxy localhost:4000
    }
    handle {
        reverse_proxy localhost:4001
    }
}

# Grafana
grafana.example.com {
    reverse_proxy localhost:3000 {
        header_up Host {upstream_hostport}
    }
}

# Nextcloud — special handling needed
nextcloud.example.com {
    reverse_proxy localhost:8080

    # Nextcloud requires these headers
    header {
        Strict-Transport-Security "max-age=31536000"
    }

    redir /.well-known/carddav /remote.php/dav 301
    redir /.well-known/caldav /remote.php/dav 301
}

Local Development HTTPS

# Install Caddy's local CA to system trust store
sudo caddy trust

# Create local Caddyfile
cat > Caddyfile << 'EOF'
localhost {
    reverse_proxy :3000
}

myapp.localhost {
    reverse_proxy :4000
}
EOF

# Run Caddy locally
caddy run

# Now https://localhost and https://myapp.localhost work with valid TLS

Generating Password Hashes for Basic Auth

# Interactive password entry
caddy hash-password

# Pipe password
echo "mysecretpassword" | caddy hash-password --stdin

# Use in Caddyfile
example.com {
    basicauth /secure/* {
        alice <HASH_FROM_ABOVE>
    }
    file_server
}

Tips and Best Practices

PracticeDetails
Validate before reloadAlways run caddy validate before applying config changes
Use environment variablesStore secrets in env vars and reference as {$VAR} in Caddyfile
Check logsCaddy logs to stdout by default; configure output file for persistent logs
Staging CA for testingUse ca https://acme-staging-v02.api.letsencrypt.org/directory to avoid rate limits
encode gzip zstdAlways add compression for text-based responses
Remove Server headerUse header -Server to hide Caddy version info
Mount /data as a volumeCaddy stores certs in /data — persist this volume in Docker
Use handle not locationCaddy’s handle blocks are cleaner than nginx-style location blocks
Admin API securityRestrict admin API to localhost or use admin off in production
DNS challenge for wildcardsWildcard certs require DNS provider plugin for ACME DNS-01 challenge
# Useful one-liners
caddy file-server --browse --listen :8080   # Quick file server
caddy reverse-proxy --from :8080 --to localhost:3000  # Quick reverse proxy
caddy run --watch                            # Auto-reload on config change
journalctl -u caddy -f                       # Follow systemd logs
curl -s http://localhost:2019/config/ | jq . # View running JSON config