Why Caddy Beats the Traditional Reverse Proxy Setup
Running a reverse proxy at home or on a VPS used to mean wrestling with Nginx config files, manually wiring up Certbot, and setting renewal cron jobs that would quietly break six months later. Caddy flips that entirely. It handles SSL certificate issuance and renewal automatically through Let’s Encrypt, stores everything in a single readable config file, and reloads without downtime. For anyone self-hosting services like media servers, dashboards, or personal apps behind a domain, that combination removes most of the friction that used to make the setup feel brittle.
This guide walks through installing Caddy on a Linux server, configuring it as a reverse proxy for one or more backend services, and enabling automatic HTTPS – all from scratch. The steps assume a Debian or Ubuntu-based system with a domain name already pointed at your server’s IP address. If you’re building out a self-hosted stack (for example, running something like Immich as a self-hosted Google Photos alternative), Caddy makes a practical front door for those services.

Installing Caddy on Debian or Ubuntu
Caddy is not in the default Ubuntu or Debian repositories, but its maintainers provide an official apt repository that makes the install clean and straightforward. Start by installing the required dependencies and adding the Caddy GPG key so your system can verify packages from that source.
Run the following commands in order:
- Install prerequisites:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl - Add the Caddy GPG key:
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg - Add the repository:
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list - Update and install:
sudo apt update && sudo apt install caddy
Once installed, Caddy starts automatically as a systemd service. You can verify it is running with sudo systemctl status caddy. The default config at /etc/caddy/Caddyfile serves a placeholder page on port 80. That file is where all your reverse proxy configuration will live.
Writing the Caddyfile for Reverse Proxy and SSL
The Caddyfile syntax is intentionally minimal. Unlike Nginx, where a basic SSL reverse proxy block can run to 20 or 30 lines with separate server blocks for HTTP and HTTPS, Caddy condenses the same configuration into three or four lines. Automatic HTTPS is on by default – if your domain resolves to the server, Caddy will request and install a certificate from Let’s Encrypt without any extra directives.
Open the default config file with sudo nano /etc/caddy/Caddyfile and replace its contents with a block like this:
- Basic structure: The domain name on its own line acts as the site address and triggers HTTPS automatically.
- reverse_proxy directive: Points Caddy to the backend service running on a local port, such as
reverse_proxy localhost:8096for a media server on port 8096. - Multiple services: Each domain gets its own block. You can stack as many as you need in the same file without them interfering.
A working example for two services looks like this:
photos.yourdomain.com { reverse_proxy localhost:2283 }media.yourdomain.com { reverse_proxy localhost:8096 }
Save the file and run sudo caddy fmt --overwrite /etc/caddy/Caddyfile to auto-format it, then reload the service with sudo systemctl reload caddy. Caddy will immediately reach out to Let’s Encrypt, complete the ACME challenge, and begin serving both domains over HTTPS. Certificate renewal happens in the background without any intervention required.

If your backend service runs on a different machine on your local network rather than on localhost, the reverse_proxy directive accepts internal IP addresses just as easily – for example, reverse_proxy 192.168.1.50:8080. This is useful in homelab setups where different services run on separate machines or containers, and you want Caddy on one box handling all SSL termination centrally.
Handling Common Configuration Additions
The basic reverse proxy block handles most use cases, but a few extra directives regularly come up in production setups. Header manipulation is the most common. Some applications need the original client IP passed through, which you do with header_up X-Real-IP {remote_host} inside the site block. WebSocket support, needed by apps like home automation dashboards, works without any extra config because Caddy handles connection upgrades automatically.
Rate limiting and access control require additional modules not bundled with the standard Caddy binary, but basic IP allowlisting is available with the built-in @ matcher syntax. For example, you can restrict a service to a single IP range:
- Define a matcher:
@blocked not remote_ip 192.168.0.0/24 - Return a 403 for everything outside it:
respond @blocked 403
This approach works well for internal admin panels or services that should never be publicly reachable, even though they sit behind a valid domain and SSL certificate.
Troubleshooting Certificate and DNS Errors
The most common failure point is DNS. Caddy uses the HTTP-01 ACME challenge by default, which means Let’s Encrypt sends a request to port 80 of your domain to verify ownership. If your domain’s A record doesn’t point to the server yet, or if a firewall blocks port 80, the challenge will fail and Caddy will log an error. Check this with sudo journalctl -u caddy --no-pager -n 50 to see what the ACME client returned.

Wildcard certificates require the DNS-01 challenge instead, which means using a Caddy build that includes your DNS provider’s plugin. The standard apt package doesn’t include these, so you’d need to download a custom build from the Caddy download page at caddyserver.com and configure the tls block with your provider’s API credentials. For most single-service setups, sticking with individual subdomain certificates and the standard binary avoids that complexity entirely.
One thing worth keeping in mind: Let’s Encrypt enforces rate limits on certificate issuance – specifically, five duplicate certificates per week for the same domain. During testing, use the acme_ca https://acme-staging-v02.api.letsencrypt.org/directory directive inside your tls block to hit Let’s Encrypt’s staging environment instead. Staging certificates aren’t trusted by browsers, but they let you verify the full challenge process without burning through your production quota. Once everything works, remove the staging directive and reload.
Frequently Asked Questions
Does Caddy automatically renew SSL certificates?
Yes. Caddy handles Let’s Encrypt certificate renewal in the background without any cron jobs or manual steps required.
Can Caddy proxy to services on other machines in my network?
Yes. The reverse_proxy directive accepts internal IP addresses like 192.168.1.50:8080, not just localhost.





