IAnswerable

Caddy Gives You HTTPS With Two Lines

18 Sep 2026

This is a complete, working HTTPS site in Caddy:

example.com {
    reverse_proxy localhost:3000
}

Save that as a Caddyfile, run caddy run, and Caddy gets a certificate from Let’s Encrypt, serves it, renews it before it expires, and redirects plain HTTP to HTTPS. No certbot, no cron job, no reload hook.

Why this works without extra setup

Caddy implements the ACME protocol itself. When it starts and sees a domain in the config, it requests a certificate directly from Let’s Encrypt (or ZeroSSL as a fallback), proves ownership over HTTP or TLS challenge, and stores the cert on disk. It checks expiry on its own schedule and renews automatically, well before the 90-day cutoff.

You don’t configure any of that. It’s the default behavior the moment a config block has a real domain name in it.

Getting it running

1. Point DNS at the box

The ACME HTTP challenge needs port 80 reachable from the internet, and the domain needs an A or AAAA record pointing at your server. If DNS isn’t propagated yet, the challenge fails and Caddy retries with backoff — it doesn’t crash, it just won’t have a cert yet.

2. Install and run

sudo apt install -y caddy
sudo systemctl enable --now caddy

Edit /etc/caddy/Caddyfile:

example.com {
    reverse_proxy localhost:3000
}
sudo systemctl reload caddy

Within a few seconds you should have a valid cert. Check it:

curl -I https://example.com

3. Multiple sites, one file

api.example.com {
    reverse_proxy localhost:8080
}

app.example.com {
    reverse_proxy localhost:3000
}

Each block gets its own certificate. Caddy handles them independently — one failing (say, DNS not ready yet) doesn’t block the others.

When Caddy is the wrong tool

If you’re behind a network that blocks outbound port 80/443 to Let’s Encrypt’s servers, or the box has no public DNS at all (internal-only service), automatic HTTPS can’t work — there’s no way for Let’s Encrypt to verify you own the domain. Use internal CA certs or a self-signed cert with tls internal instead:

internal.example.local {
    tls internal
    reverse_proxy localhost:3000
}

That skips ACME entirely and self-signs, which is right for internal services but means browsers will warn on first visit until you trust the CA.

If you’re rate-limited by Let’s Encrypt — it caps certificates per domain per week — repeatedly reloading a broken config with a real domain can burn through that limit. Use the Let’s Encrypt staging environment while testing:

{
    acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

Staging certs aren’t trusted by browsers, but they don’t count against your production rate limit.

The gotcha

If Caddy can’t get a cert — DNS not propagated, port 80 blocked by a firewall, rate limit hit — it doesn’t fall back to serving HTTP. The site is just down until the cert succeeds. Check journalctl -u caddy -f when a new domain isn’t coming up; the ACME failure reason is always in there.