Caddy Reverse Proxy with Automatic HTTPS for Multiple Apps
Web Infrastructure

Caddy Reverse Proxy with Automatic HTTPS for Multiple Apps

  • AuthorLiam K.
  • Date
  • Time17 minutes

Caddy is a modern web server and reverse proxy that provisions and renews TLS certificates automatically. Compared to manual Certbot workflows, Caddy reduces operational overhead while keeping strong defaults for HTTPS redirects, HTTP/2, and optional HTTP/3. This guide walks through a production-ready multi-app setup on Ubuntu or Debian.

Prerequisites

  • Ubuntu 22.04+ or Debian 12 server with a public IP
  • DNS A/AAAA records pointing to the server for each hostname
  • Backend apps listening on localhost (e.g. ports 3000 and 8080)
  • Ports 80 and 443 open in your firewall

Step 1: Install Caddy

bash
sudo apt update
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

Step 2: Create the Caddyfile

Replace the example domains with your own. Caddy obtains certificates on first request and renews them automatically before expiry.

bash
sudo tee /etc/caddy/Caddyfile >/dev/null <<'EOF'
{
    email admin@example.com
    admin off
}

(common_headers) {
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy strict-origin-when-cross-origin
        -Server
    }
}

app.example.com {
    import common_headers
    encode gzip zstd
    reverse_proxy 127.0.0.1:3000
}

api.example.com {
    import common_headers
    encode gzip zstd
    reverse_proxy 127.0.0.1:8080 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }
}
EOF

Step 3: Validate and Reload

bash
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl enable caddy
sudo systemctl reload caddy
sudo systemctl status caddy --no-pager

Step 4: Verify TLS and Routing

bash
curl -I https://app.example.com
curl -I https://api.example.com/health
sudo journalctl -u caddy -n 50 --no-pager

Step 5: Enable HTTP/3 (Optional)

HTTP/3 uses QUIC over UDP port 443. Open UDP 443 in your firewall before enabling it.

bash
sudo ufw allow 443/udp
# Add to the global options block in /etc/caddy/Caddyfile:
# {
#     servers {
#         protocols h1 h2 h3
#     }
# }
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Step 6: Add Basic Rate Limiting

bash
# Install rate limit module if not bundled, then add to a site block:
# rate_limit {
#     zone api_limit {
#         key {remote_host}
#         events 100
#         window 1m
#     }
# }

Production Checklist

  • Keep backend services bound to 127.0.0.1 — only Caddy should be public-facing.
  • Monitor certificate renewal events in journalctl -u caddy.
  • Use separate hostnames per service to isolate blast radius and simplify routing.
  • Test config changes with caddy validate before every reload.
  • Back up /etc/caddy/Caddyfile and document ownership for on-call.

"Automatic HTTPS only saves time when your proxy config is version-controlled, validated, and owned by the same team that runs production."

Technical Author

Liam K.

Liam K.

Founder of Trivox. Writes about how the platform, the network, and the Amsterdam sites fit together.