Reverse Proxy Done Right: Automatic HTTPS with Caddy in Ten Minutes
TLS certificates that renew themselves

If you have ever wrestled with certificate files, cron jobs that renew them, and a configuration syntax that feels designed to punish typos, you will appreciate what follows. There is a web server that obtains valid HTTPS certificates for you, renews them before they expire, and routes traffic to your applications using a configuration file short enough to read in a single breath. It is called Caddy, and by the end of this guide you will have two services sitting safely behind it with proper TLS, no manual certificate handling, and roughly ten minutes of effort. This is what a reverse proxy is supposed to feel like.
1 What a Reverse Proxy Does and Why You Want One
A reverse proxy is a server that sits in front of your applications and forwards incoming requests to them. To the outside world it looks like a single, tidy front door; behind it you might run a dozen separate services on a jumble of internal ports. When a request arrives for app.example.com, the proxy decides which internal service should handle it and passes the request along, then relays the response back.
This pattern earns its keep in several ways. It gives you one place to terminate HTTPS, so individual applications need not each manage their own certificates. It lets you host many services on a single public IP address by routing on hostname. It centralises concerns like compression, headers, and access control. And it hides the messy internal topology of your network behind a clean, consistent interface. Almost every serious deployment uses one, whether the operators call it that or not.
2 Caddy Versus Nginx
Nginx is the venerable workhorse of the reverse-proxy world, and it is superb: fast, battle-tested, and capable of almost anything. But its configuration is verbose, and crucially it does nothing about certificates on its own. To get HTTPS with Nginx you typically bolt on Certbot, schedule renewals, and hope the moving parts stay aligned.
Caddy’s headline feature is that HTTPS is automatic and on by default. When you tell Caddy to serve a domain, it contacts Let’s Encrypt, completes the challenge, installs the certificate, and quietly renews it for the rest of time without you lifting a finger. The configuration file, called a Caddyfile, is dramatically terser than the Nginx equivalent. Nginx still wins on raw throughput at extreme scale and on the sheer breadth of its ecosystem, but for the homelab, the small business, and the developer who wants TLS to simply work, Caddy is hard to beat.
3 Installing Caddy
On a Debian or Ubuntu system, Caddy provides an official repository so you get signed packages and clean upgrades:
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 caddyThe package installs Caddy as a systemd service that is already running and pointed at a configuration file in /etc/caddy/Caddyfile. You can confirm it is alive:
systemctl status caddy4 A Minimal Caddyfile for Two Services
Here is the whole point of the exercise. Suppose you have two applications running locally: a dashboard on port 3000 and an API on port 8080. You own example.com and want each on its own subdomain with automatic HTTPS. Edit /etc/caddy/Caddyfile to read:
dashboard.example.com {
reverse_proxy localhost:3000
}
api.example.com {
reverse_proxy localhost:8080
}That is the entire configuration. Reload Caddy to apply it:
sudo systemctl reload caddyThe moment you reload, Caddy notices the two public domains, reaches out to Let’s Encrypt, solves the challenge over port 80 or 443, and installs certificates. Visit https://dashboard.example.com and you are greeted with a valid padlock and your application behind it. No certificate files, no renewal scripts, no separate tooling. For this to work the domains must already resolve to your server’s public IP, which is the one piece of homework you do beforehand.
5 Wildcard Certificates via the DNS Challenge
The default challenge requires your server to be reachable on port 80 or 443 from the public internet. Sometimes that is impossible, perhaps because your services live behind a firewall, or because you want a single wildcard certificate covering *.example.com rather than one per subdomain. For these cases Caddy supports the DNS challenge, where it proves ownership by creating a temporary record in your DNS zone instead of answering an HTTP request.
This requires a Caddy build that includes the plugin for your DNS provider, available from the download page or via xcaddy. With the provider module in place, a wildcard configuration looks like this:
*.example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
reverse_proxy app1 localhost:3000
}You supply an API token scoped to edit your DNS zone, and Caddy handles the rest. The DNS challenge is also the only way to obtain certificates for internal-only services that never face the public web. A wildcard certificate has the further advantage of not enumerating every subdomain in public certificate transparency logs, which slightly reduces how much a curious observer can learn about your internal layout.
6 Headers and Compression
A reverse proxy is the natural place to apply cross-cutting niceties, and Caddy makes them concise. Enabling compression and adding sensible security headers takes only a few directives:
dashboard.example.com {
encode gzip zstd
header {
Strict-Transport-Security "max-age=31536000;"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
reverse_proxy localhost:3000
}The encode directive compresses responses on the fly, the header block hardens the browser’s behaviour, and the -Server line strips the header that would otherwise advertise what you are running. These are the kinds of touches that are tedious to retrofit into every application but trivial to apply once at the proxy.
7 Common Pitfalls
The failures people hit are nearly always one of a small handful. The first is ports: Caddy needs inbound 80 and 443 open for the standard challenge, so check your firewall and any router port forwarding. If certificate issuance hangs, this is the usual culprit.
The second is DNS. The domain must resolve to your server before Caddy can prove ownership, and freshly changed records can take time to propagate. Verify with dig dashboard.example.com and make sure the answer is your actual public address.
The third is rate limits. Let’s Encrypt limits how many certificates you can request for a domain in a given week, and a misconfiguration that retries in a loop can exhaust that allowance, leaving you temporarily blocked for several days. This catches people who tear down and rebuild a server repeatedly while experimenting, each attempt burning through the weekly budget. The fix is to do your experimenting against the staging environment, which issues untrusted certificates but has far looser limits, and only switch to production once the configuration is settled. While testing, point Caddy at the staging certificate authority so failed attempts do not count against you:
{
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}Remove that global block once everything works, reload, and Caddy will fetch real, trusted certificates.
8 Ten Minutes Well Spent
The promise at the top was modest: two services, real HTTPS, no certificate babysitting, in roughly ten minutes. Caddy delivers on it because it inverts the usual priorities, treating encryption as the default rather than an advanced add-on. You write a few lines describing which domain maps to which service, and the tedious machinery of certificate issuance and renewal simply vanishes into the background. Once you have experienced a reverse proxy that manages its own TLS, the old way of cron jobs and manual renewals starts to look like a chore you are glad to have left behind.