Set Up Caddy as a Reverse Proxy
Caddy makes HTTPS and reverse proxying effortless. No certificate headaches, no confusing config files — just a few lines and you're live.
What is Caddy?
Caddy is a web server and reverse proxy written in Go. Unlike Nginx or Apache, Caddy automatically obtains and renews TLS certificates from Let's Encrypt with zero configuration. You don't need to install certbot, write cron jobs, or mess with certificate files — Caddy handles it all behind the scenes.
It also handles reverse proxying — forwarding incoming web requests to backend services running on your server — with a single line of config. If you're self-hosting multiple services behind one domain, Caddy is the simplest way to give each one HTTPS and a friendly URL.
- •Automatic HTTPS with zero config — certificates are obtained, installed, and renewed for you
- •Reverse proxy in one line — forward requests to any backend service
- •HTTP/2 and HTTP/3 support out of the box for faster page loads
- •Simple, human-readable configuration format called the Caddyfile
Step 1. Install Caddy via Docker
The easiest way to get started is with Docker. This keeps Caddy isolated and makes updates painless. Create a directory for your Caddy setup, then add a docker-compose file.
mkdir ~/caddy && cd ~/caddy
Create your docker-compose.yml:
services:
caddy:
image: caddy:latest
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./data:/data
- ./config:/config
Port 80 is for HTTP (used during certificate issuance), port 443 is for HTTPS, and the UDP port 443 enables HTTP/3. The volumes mount your configuration file and persistent storage for certificates. Start it with:
docker compose up -d
Step 2. Understand the Caddyfile
Caddy's configuration lives in a file called Caddyfile. Its syntax is remarkably simple — most setups need only a few lines.
Here's a static site in three lines. Caddy automatically gets an HTTPS certificate and serves your files:
mysite.com {
root * /var/www/html
file_server
}
Reverse proxying to a local service is even shorter — just two lines to forward requests to an app running on port 3000:
app.mysite.com {
reverse_proxy localhost:3000
}
That's it. Caddy handles the TLS certificate for app.mysite.com automatically, and every request hitting that subdomain gets forwarded to port 3000 with full HTTPS.
Step 3. Reverse Proxy Your Self-Hosted Apps
Once you understand the basics, wiring up multiple services is straightforward. Each subdomain points to a different backend, and every one gets its own HTTPS certificate — all automatic.
Here's a complete Caddyfile routing three popular self-hosted apps to friendly subdomains:
jellyfin.yourdomain.com {
reverse_proxy localhost:8096
}
nextcloud.yourdomain.com {
reverse_proxy localhost:8080
}
immich.yourdomain.com {
reverse_proxy localhost:2283
}
homepage.yourdomain.com {
reverse_proxy localhost:3000
}
After saving your Caddyfile, reload the configuration without downtime:
docker exec -w /etc/caddy caddy caddy reload
Within seconds, all four subdomains will have valid HTTPS certificates. No certbot, no cron jobs, no manual renewal — Caddy checks and renews certificates automatically before they expire.
Step 4. Add Security Headers and Hardening
Caddy makes it easy to add security headers that protect your visitors and improve your security score. You can define a reusable snippet and apply it across all your sites.
Define a security header snippet at the top of your Caddyfile, then import it into each site block:
(security_headers) {
header {
Strict-Transport-Security "max-age=63072000"
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
jellyfin.yourdomain.com {
import security_headers
reverse_proxy localhost:8096
}
For rate limiting — useful on login pages or API endpoints — Caddy supports this with a few extra lines:
nextcloud.yourdomain.com {
import security_headers
rate_limit {
zone login_zone {
key {remote_host}
events 20
window 1m
}
}
reverse_proxy localhost:8080
}
This limits each visitor to 20 requests per minute, protecting your services from brute-force attempts.
Step 5. Going Further with Caddy
Caddy has a rich ecosystem of plugins and advanced features. Here are a few worth exploring once you're comfortable with the basics:
- •Cloudflare DNS integration — Use the Cloudflare plugin to obtain wildcard certificates (*.yourdomain.com) via DNS challenge. This is essential if port 80 isn't reachable from the internet or you need certificates for internal-only services.
- •Load balancing — Distribute traffic across multiple backend servers with Caddy's built-in load balancing. Just list multiple upstreams in your reverse_proxy directive.
- •Health checks — Caddy can actively monitor your backends and stop sending traffic to unhealthy ones. Add a health check path to any reverse_proxy block.
- •Prometheus metrics — Enable the Prometheus plugin to expose request metrics for monitoring and dashboards. Great for keeping an eye on traffic patterns and error rates.
- •Custom error pages — Replace default error pages with your own branded HTML. Caddy's handle_errors directive makes this a one-liner.
Quick Tips
- •Use
caddy fmt --overwrite Caddyfileto auto-format your config before reloading — it catches syntax errors early. - •Caddy's data directory holds your certificates. Back it up! Losing it means re-issuing all certs, and Let's Encrypt has rate limits.
- •Test new configs with
caddy validatebefore reloading. A broken config can take down all your proxied services. - •If a service is slow, add
flush_interval -1to the reverse_proxy directive to enable response buffering — this can dramatically improve perceived speed. - •Caddy's JSON config API lets you change routes on the fly without touching the Caddyfile — ideal for automated deployments and dynamic routing.
Continue Reading
Ready to LEVEL UP?
Now that your reverse proxy is humming, secure your Docker stack and lock down your domains.
Browse All How-To Guides