Logo

Caddy: automatic HTTPS reverse proxy

Caddy: automatic HTTPS reverse proxy

Caddy does HTTPS reverse proxy with zero manual certificate config. ACME, HTTP/2, HTTP/3, gzip, brotli, OCSP, automatic. The web server that "just works".

Introduction

Caddy is a modern web server written in Go with one obsession: zero config for HTTPS. Specifically:

  • Auto-fetches Let's Encrypt / ZeroSSL certs
  • Auto-renews before expiration
  • HTTPS by default
  • HTTP/2 and HTTP/3 (QUIC) by default
  • Simple reverse proxy
  • Ultra-readable Caddyfile syntax
  • Hot-reload without downtime

Ideal for: personal sites, dev, simple prod, prototyping. For complex configs, Nginx remains the reference.

Prerequisites

  • Linux VPS
  • Ports 80 and 443 open (required for ACME)
  • Domain pointing to VPS
  • Root access

Step 1: Installation

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

caddy version
sudo systemctl status caddy

Step 2: The Caddyfile

All config in /etc/caddy/Caddyfile:

your-domain.com {
    respond "Hello, World!"
}
sudo systemctl reload caddy

Visit https://your-domain.com: Caddy got a Let's Encrypt cert automatically.

⚠️ Zero SSL config needed.

Step 3: Serve static files

your-domain.com {
    root * /var/www/html
    file_server
}

With directory listing:

your-domain.com {
    root * /var/www/html
    file_server browse
}

Step 4: Reverse proxy to app

api.your-domain.com {
    reverse_proxy localhost:3000
}

That's all. HTTPS, HTTP/2, HTTP/3, 80→443 redirect. All handled.

Multiple backends:

api.your-domain.com {
    reverse_proxy backend1:3000 backend2:3000 backend3:3000 {
        lb_policy round_robin
        health_uri /healthz
        health_interval 10s
    }
}

Step 5: Multi-site

site1.com {
    root * /var/www/site1
    file_server
}

site2.com {
    reverse_proxy localhost:8000
}

api.site2.com {
    reverse_proxy backend:3000
}

Each site gets its own automatic cert.

Step 6: Advanced configs

PHP with PHP-FPM

your-domain.com {
    root * /var/www/html
    php_fastcgi unix//run/php/php8.2-fpm.sock
    file_server
}

Compression

your-domain.com {
    encode gzip zstd
    reverse_proxy localhost:3000
}

Security headers

your-domain.com {
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
    reverse_proxy localhost:3000
}

Rate limiting (via plugin)

sudo caddy add-package github.com/mholt/caddy-ratelimit
your-domain.com {
    rate_limit {
        zone main {
            key {client_ip}
            events 100
            window 1m
        }
    }
    reverse_proxy localhost:3000
}

Step 7: Wildcard certificate

Requires DNS-01 challenge:

sudo caddy add-package github.com/caddy-dns/cloudflare
*.your-domain.com {
    tls {
        dns cloudflare your_cloudflare_token
    }
    
    @app1 host app1.your-domain.com
    handle @app1 {
        reverse_proxy localhost:3001
    }
    
    @app2 host app2.your-domain.com
    handle @app2 {
        reverse_proxy localhost:3002
    }
}

Step 8: Logging

your-domain.com {
    log {
        output file /var/log/caddy/access.log {
            roll_size 100mb
            roll_keep 10
        }
        format json
    }
    reverse_proxy localhost:3000
}
sudo tail -f /var/log/caddy/access.log | jq

Step 9: Basic auth

admin.your-domain.com {
    basicauth /* {
        admin $2a$14$Zkx19XLiW6VYouLHR5NmfO...
    }
    reverse_proxy localhost:8080
}
caddy hash-password

Step 10: WebSockets

No extra config:

ws.your-domain.com {
    reverse_proxy localhost:3000
}

Step 11: Admin API

Caddy exposes a REST API on :2019 (localhost only by default):

curl localhost:2019/config/ | jq

curl -X POST localhost:2019/config/apps/http/servers/srv0/routes \
    -H "Content-Type: application/json" \
    -d '{"match": [{"host": ["new.example.com"]}], "handle": [{"handler": "reverse_proxy", "upstreams": [{"dial": "localhost:9000"}]}]}'

Step 12: Migration from Nginx

caddy adapt --config /etc/nginx/sites-available/my-site --adapter nginxconf

Get an equivalent Caddyfile. Verify before deploying.

Troubleshooting

Cert won't generate

Check:

  • Port 80 is open (HTTP-01)
  • DNS points to VPS
  • No other service on port 80
sudo journalctl -u caddy -f

502 Bad Gateway

Backend down. Check:

curl http://localhost:3000

"address already in use"

Another service uses 80/443. Stop it.

Let's Encrypt rate limit hit

Switch to staging temporarily:

your-domain.com {
    tls {
        ca https://acme-staging-v02.api.letsencrypt.org/directory
    }
}

Useful commands

sudo systemctl status caddy
sudo systemctl reload caddy
caddy validate --config /etc/caddy/Caddyfile
caddy fmt /etc/caddy/Caddyfile --overwrite
sudo journalctl -u caddy -f
ls /var/lib/caddy/.local/share/caddy/certificates/
curl localhost:2019/config/

Conclusion

Caddy is:

  • The "just works" web server (no SSL config)
  • Suited for simple to medium sites
  • Very fast for prototyping
  • Rich plugins (DNS providers, rate limit, WAF)

Limits: complex Nginx configs (regex, nested locations, OpenResty, Lua) → Nginx better.

Going further:

Resources

Join our Discord community server

For any questions, suggestions, or just to chat with the community, join us on Discord!

900+Members