You deploy an app to a real server and the browser still warns "not secure." So you install certbot, generate a certificate, wire it into the nginx config, set up a cron job to renew it, and at some point you forget the HTTP to HTTPS redirect and every visitor hits a redirect loop. The certificate dance is the most boring part of running a server, and it keeps being your problem.
Caddy is the web server that makes this whole category of work disappear. Point it at a domain and it obtains, installs, and renews TLS certificates on its own. No certbot, no cron, no redirect rules. It also serves static files and reverse proxies to your app, all from one small config file called a Caddyfile.
This walkthrough covers the five things you will actually do with it: install Caddy, host a static site with automatic HTTPS, reverse proxy to a backend, load balance two backends with health checks, and protect a path with Basic Auth. Every config below was tested against Caddy v2.11.4, the current stable release as of June 2026.
Prerequisites
- A Linux server. Ubuntu or Debian works for the install steps here
- A domain name with an A or AAAA record pointing to the server's public IP
- Ports 80 and 443 reachable from the internet. Caddy needs them because the certificate authority contacts your server over port 80 to prove you own the domain
- Comfort with a terminal. That is the whole skill requirement
Step 1: Install Caddy
Caddy publishes official packages for Debian and Ubuntu. Add the repository, then install:
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 chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
Those lines come straight from the official install page. The package registers a systemd service, so Caddy runs and restarts like any proper service.
Check what you got:
caddy version
The current stable line is v2, and the newest release at the time of writing is v2.11.4.
Step 2: Write your first Caddyfile
Caddy reads a file called Caddyfile, and caddy run expects it in the current directory. On the Debian package, the service runs with the config at /etc/caddy/Caddyfile.
The format is a site address followed by indented directives. For a static site:
example.com {
root * /var/www/example
file_server
}
Two lines of config. root sets the folder, file_server serves it over HTTP. Because the site name is a real public domain, Caddy also turns on automatic HTTPS for it and redirects HTTP traffic to HTTPS.
Apply changes to the running server with:
sudo systemctl reload caddy
Use reload, not restart. The docs are blunt about this: stopping the server to change config in production causes downtime. caddy reload swaps the configuration without dropping connections.
Step 3: Automatic HTTPS, and the two things it needs from you
You did not create a certificate anywhere, yet the site is served over HTTPS with a valid one. That is automatic HTTPS doing its job.
Here is what happens. Caddy talks to a public ACME certificate authority, Let's Encrypt or ZeroSSL by default, and requests a certificate for your domain. The authority runs an ACME challenge to confirm you control the domain, and Caddy answers it automatically. Let's Encrypt certificates are valid for 90 days, and Caddy renews the certificates it manages before they expire, in the background, forever. You never touch it.
Two setup requirements decide whether this works:
- The DNS record for your domain must point at this server. Caddy performs an authoritative DNS lookup of your hostname as part of the HTTP challenge.
- Port 80 must be open. The authority reaches you over port 80 to deliver the challenge, and Caddy also uses port 80 for the HTTP to HTTPS redirect.
If the DNS record is wrong, certificate provisioning fails. Caddy keeps retrying in the background, but set the DNS record first and save yourself the confusion.
Testing on a server that is not live yet? The docs recommend switching the ACME endpoint to the Let's Encrypt staging server. The production endpoint has rate limits, and hitting them during experiments can block certificate issuance for up to a week.
Step 4: Reverse proxy to your app
Your app is a Node, Go, or Python service listening on some port, say localhost:8080. Expose it with:
api.example.com {
reverse_proxy localhost:8080
}
Requests to api.example.com are forwarded to the app on port 8080. WebSockets work without extra config, Caddy handles the upgrade. The same one-liner proxies a FastAPI app on port 8000, a Node server on port 3000, or a Spring Boot app on 8080. The language never matters.
Step 5: Load balance two backends, with health checks
reverse_proxy accepts a list of upstreams, and Caddy load balances across them by default:
app.example.com {
reverse_proxy 10.0.0.11:8080 10.0.0.12:8080
}
Traffic spreads across both servers. On its own, though, that still sends requests to a backend that is down. Active health checks fix that:
app.example.com {
reverse_proxy 10.0.0.11:8080 10.0.0.12:8080 {
health_uri /health
health_interval 10s
health_timeout 5s
}
}
Caddy hits GET /health on each backend every 10 seconds. A backend that does not answer within 5 seconds is marked down and stops receiving traffic until it responds again. The defaults are a 30 second interval and a 5 second timeout, so the example tightens both. If your app has no /health endpoint, add one. It is the hook every load balancer expects.
I tested this exact pattern locally with two backends and a 2 second interval. When I killed the second backend, the health checker logged a failed probe every 2 seconds and Caddy routed every request to the surviving one. No request hit the dead server.
Step 6: Protect a path with Basic Auth
Caddy has Basic Auth built in, and it refuses plaintext passwords, so hash first:
caddy hash-password
The command asks for a password and prints a bcrypt hash. You can also pass it directly: caddy hash-password --plaintext 's3cret-pass'. Paste the hash into the Caddyfile with a username and a matcher that targets only the path you want to protect:
example.com {
root * /var/www/example
file_server
@private path /admin/*
basic_auth @private {
arya $2a$14$ilwb1R4PDutllnO73dRiAORmyjHWwO54hpPjHnKqm/OyyScOoUs/S
}
}
Visitors to /admin/ get a browser password prompt. Wrong credentials get HTTP 401 Unauthorized. bcrypt is the default hash algorithm, and argon2id is available if you want the stronger option. Do not put a plaintext password in this file, Caddy will reject the config.
Step 7: Keep it running and check your work
The official package installs a systemd unit. Enable it so Caddy starts on boot:
sudo systemctl enable --now caddy
systemctl status caddy
Logs go to the journal:
journalctl -u caddy -f
Before applying a change you are not sure about, validate the config without touching the running server:
caddy validate --config /etc/caddy/Caddyfile
Or see exactly what Caddy converts your Caddyfile into:
caddy adapt --config /etc/caddy/Caddyfile
That prints the JSON config Caddy actually runs, which is a good way to learn what the Caddyfile shorthand means.
When to use Caddy vs the alternatives
nginx: pick it when you already live inside its ecosystem, need very custom request routing, URL rewriting, or a module Caddy does not ship. nginx has decades of extensions and tuning options. Caddy beats it badly on TLS setup and on config brevity, but nginx wins when you need fine-grained control.
Traefik: it also does automatic certificates, but it is built around dynamic service discovery. It reads labels from containers in Docker and Kubernetes and reconfigures itself as services come and go. If your workloads are containerized and churn constantly, Traefik fits. If you run a handful of servers with a small set of services, a single Caddyfile is less machinery than Traefik's whole model.
Caddy's honest weaknesses: it is younger, so the plugin ecosystem is thinner, and its config model, site address then directives, is less expressive for unusual request handling. For static sites, reverse proxying, and HTTPS, it is dramatically less work.
Verdict
Caddy is the fastest way to put something on the internet with HTTPS that stays valid. Two lines for a static site. One line for a reverse proxy. Zero manual certificate work, because renewal stopped being your problem the moment you installed it.
Start with one domain pointed at your server and let TLS happen on its own. Then add a reverse proxy to whatever runs locally, and forget the certificate dance entirely.