Skip to main content

Caddy Server: Auto-HTTPS and Reverse Proxy Without the Pain

Cloud & DevOps · 19.04.2026 · 4 min read
Illustration for “Caddy Server: Auto-HTTPS and Reverse Proxy Without the Pain”

Caddy Server: a modern web server

Caddy automatically obtains and renews SSL certificates via Let's Encrypt without any manual configuration. This is the main advantage over Nginx — no Certbot needed.

Installation on Ubuntu/Debian

apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install caddy

Basic configuration (Caddyfile)

# /etc/caddy/Caddyfile

yourdomain.com {
    root * /var/www/html
    file_server
    encode gzip
}

api.yourdomain.com {
    reverse_proxy localhost:3000
}

PHP-FPM with Caddy

yourdomain.com {
    root * /var/www/html
    encode gzip
    php_fastcgi unix//run/php/php8.3-fpm.sock
    file_server
    try_files {path} {path}/ /index.php?{query}
}

Security headers

yourdomain.com {
    root * /var/www/html
    file_server
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        -Server
    }
}

Load balancing

yourdomain.com {
    reverse_proxy {
        to localhost:8001 localhost:8002 localhost:8003
        lb_policy round_robin
        health_uri /ping
        health_interval 15s
    }
}

Service management

systemctl reload caddy
caddy validate --config /etc/caddy/Caddyfile
caddy fmt --overwrite /etc/caddy/Caddyfile
HTTP/3: Caddy enables HTTP/3 (QUIC) by default — it speeds up loading on mobile networks. Make sure UDP port 443 is open in the firewall.

Real Caddy Errors Explained

Caddy strives to be predictable, but two classes of problems show up regularly: insufficient privileges on privileged ports, and a failed automatic certificate issuance. Let's look at the exact log messages.

Permission Denied on Startup

{"level":"fatal","msg":"listen tcp :443: bind: permission denied"}

Ports below 1024 require root privileges or the CAP_NET_BIND_SERVICE capability. Caddy's official systemd unit already includes that capability via AmbientCapabilities, so this usually shows up when running the binary manually (caddy run as a regular user) or in a container without --cap-add=NET_BIND_SERVICE. The fix: start it through systemctl start caddy, or add the capability explicitly with setcap 'cap_net_bind_service=+ep' /usr/bin/caddy.

The ACME Challenge Fails

{"level":"error","msg":"job failed","error":"leaf certificate creation failed for yourdomain.com: ... [400] urn:ietf:params:acme:error:connection: Fetching http://yourdomain.com/.well-known/acme-challenge/xxxx: Connection refused"}

Let's Encrypt could not reach your server on port 80 for the HTTP-01 challenge. The causes are almost always the same: port 80 is closed in the firewall, the domain's DNS record has not propagated yet, or port 80 is already taken by another process (check with ss -tlnp | grep :80). Until the challenge succeeds, Caddy serves a self-signed certificate instead of a real one.

Caddy vs. Nginx vs. Traefik

All three servers solve the reverse proxy problem, but they prioritize different things.

CriterionCaddyNginxTraefik
Auto-HTTPS out of the boxYesNo, needs CertbotYes
Config formatCaddyfile, declarativenginx.conf, imperativeYAML or Docker labels
Native PHP-FPM supportYes, php_fastcgiYes, fastcgi_passNot directly
Best fit forSmall and mid-sized projects with auto-SSLHigh-load static content and proxyingDocker and Kubernetes environments

When Caddy Is Not a Good Fit

  • You need the ecosystem of mature third-party modules — ModSecurity, njs scripts, decades of ready-made configuration recipes that Nginx has.
  • The team already maintains hundreds of lines of battle-tested nginx.conf and migration would cost more than the benefit of auto-HTTPS.
  • You need raw L4 TCP/UDP proxying without building a custom binary via xcaddy with the layer4 plugin.
  • The infrastructure is entirely Docker/Kubernetes with label-based service discovery — Traefik fits more naturally there.
  • You need fine-grained on-disk caching (Nginx-style proxy_cache) — in Caddy that requires a third-party cache-handler module.

Verifying the Result

# Check syntax before applying
caddy validate --config /etc/caddy/Caddyfile

# Make sure the service is up and listening on the ports
systemctl status caddy
ss -tlnp | grep -E ':(80|443)'

# Confirm the certificate is real, not self-signed
curl -vI https://yourdomain.com 2>&1 | grep -i "subject\|issuer"

# View the latest log lines when troubleshooting
journalctl -u caddy -n 50 --no-pager

If issuer in the curl output shows CN=Caddy Local Authority instead of Let's Encrypt, the ACME challenge did not pass and the site is running on a self-signed certificate. Check the logs with journalctl and verify DNS together with port 80 separately, before restarting Caddy. If Caddy sits in front of several backends, cross-check the load distribution against the material on Docker Swarm, and it is convenient to move backend liveness monitoring into Uptime Kuma.

Was this article helpful?
← Back to Knowledge Base Ask Support