I’ve been running a small homelab for a few years now. My setup consists of a handful of services—Nextcloud, Home Assistant, a personal blog, and a few static sites—all exposed through a single reverse proxy on a Raspberry Pi 4. For the first two years I used Nginx because it’s ubiquitous, well‑documented, and I could get it working with a simple proxy_pass block. In 2025 I started to notice a few friction points that made me consider alternatives. By the end of 2025 I had migrated to Caddy, and the switch has paid off in terms of maintenance, security, and ease of use.
Below is a walk‑through of the pain points, the migration steps, and the lessons I learned that should help anyone running a similar setup.
1. The Pain Points with Nginx
1.1 Manual TLS Renewal
Nginx itself does not handle TLS certificates. I had to pair it with Certbot, which required a cron job that ran every 12 hours:
0 */12 * * * certbot renew --deploy-hook "systemctl reload nginx"
The hook worked most of the time, but I still had to monitor the logs for failures. A single mis‑configured certbot command could leave my services without a valid certificate for days.
1.2 Configuration Verbosity
Nginx’s configuration syntax is powerful but verbose. A minimal reverse‑proxy block looks like this:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
When I added a new service, I had to duplicate the block and tweak the proxy_pass URL. The more services I added, the more I had to copy‑paste and remember to update the listen and server_name directives.
1.3 Limited Auto‑Configuration
Nginx does not provide a built‑in way to auto‑configure virtual hosts. I had to write my own scripts or use third‑party tools like nginx-proxy. Those tools added complexity and sometimes conflicted with my own custom snippets.
2. Why Caddy Appeared Attractive
Caddy is a modern web server written in Go. Its key selling points for a homelab are:
- Automatic HTTPS – Caddy uses Let’s Encrypt by default and renews certificates automatically without external tools.
- Declarative, concise config – The
Caddyfilesyntax is almost one line per site. - Zero‑config TLS – No need to run separate certbot commands or manage hooks.
- Built‑in HTTP/3 support – Useful if I want to experiment with QUIC.
These features align perfectly with a home environment where I want minimal maintenance and maximum uptime.
3. Migration Steps
3.1 Install Caddy
On Debian‑based systems I used the official APT repository:
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo tee /usr/share/keyrings/caddy-stable-archive-keyring.gpg
echo 'deb [signed-by=/usr/share/keyrings/caddy-stable-archive-keyring.gpg] https://dl.cloudsmith.io/public/caddy/stable/deb/debian any main' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
The package installs a systemd unit (caddy.service) that automatically starts on boot.
3.2 Translate Nginx Blocks to Caddyfile
A typical Nginx reverse‑proxy block becomes a single line in Caddy:
example.com {
reverse_proxy localhost:8080
}
If I need to preserve headers:
example.com {
reverse_proxy localhost:8080 {
header_up Host {host}
header_up X-Real-IP {remote}
}
}
Caddy automatically adds Host and X-Real-IP headers, so the extra block is often unnecessary unless I need custom values.
3.3 Handle Static Content
For static sites I used:
static.example.com {
root * /var/www/static
file_server
}
No separate listen directive is needed; Caddy listens on port 80 and 443 by default.
3.4 Move to Systemd Service
Caddy’s systemd unit is already configured to reload on config changes. After editing the Caddyfile, I simply run:
sudo systemctl reload caddy
No need for a separate certbot hook.
3.5 Verify TLS and HTTP/3
After the switch, I checked the TLS status with:
openssl s_client -connect example.com:443 -servername example.com
The output shows a valid certificate and the ALPN protocols include h2 and h3. I also used curl -I --http3 https://example.com to confirm HTTP/3 support.
4. What I Learned
4.1 Less Surface Area for Errors
With Caddy, the only file I touch is the Caddyfile. No separate certbot scripts, no cron jobs, no manual reloads. This reduces the chance of a mis‑configured cron entry or a forgotten reload.
4.2 Faster Iteration
Adding a new service is as simple as appending a new block to the Caddyfile. I can test the new configuration with:
caddy validate
If the syntax is wrong, Caddy will point me to the exact line. Nginx requires a full reload to catch syntax errors, which can bring the whole proxy down temporarily.
4.3 Built‑in Security Defaults
Caddy ships with a hardened set of TLS defaults (e.g., only modern ciphers, HTTP/2 enabled). I didn’t have to manually set ssl_protocols or ssl_ciphers. The only security tweak I made was to enable the http3 directive explicitly, which is optional but useful for testing.
4.4 Performance Trade‑offs
Caddy is written in Go and uses a single-threaded event loop. On a Raspberry Pi 4, I observed a modest increase in memory usage (~30 MB) compared to Nginx (~20 MB). CPU usage was comparable. For my modest traffic load, the difference is negligible. If you run a high‑traffic server, benchmark both under your expected load to decide.
4.5 Community and Documentation
Nginx has a massive ecosystem, but Caddy’s documentation is very beginner‑friendly. The official docs (https://caddyserver.com/docs/) provide clear examples, and the community is active on GitHub. I found the Caddyfile reference to be more approachable than Nginx’s nginx.conf hierarchy.
5. Caveats and Troubleshooting
| Issue | Symptom | Fix |
|---|---|---|
| Caddy not listening on port 80 | systemctl status caddy shows no listening sockets |
Ensure http_port and https_port are not overridden in the config. |
| Let’s Encrypt rate limits | caddy: failed to obtain certificate |
Use a custom DNS provider or a staging environment for testing. |
| Proxying to IPv6 | Connection fails | Use reverse_proxy [::1]:8080 or reverse_proxy /ip6/::1:8080. |
| Static files not served | 404 errors | Verify the root path and file permissions. |
6. Final Thoughts
Switching from Nginx to Caddy in a home reverse‑proxy setup simplified my workflow dramatically. Automatic TLS, concise configuration, and zero‑config reloads mean I spend less time on maintenance and more time experimenting with new services.
If you’re already comfortable with Nginx and have a complex setup, the migration might require more effort. However, for a typical homelab with a handful of services, the learning curve is shallow and the payoff is high.
Feel free to reach out if you run into any hiccups during your own migration. Happy hosting!
See also
- When Nginx Reverse‑Proxy Returns 502 on Your Home‑Lab HTTPS Site: A Step‑by‑Step Fix for Certificate and Path Errors
- How to audit stale SSH keys with ssh-keygen‑l and fail2ban to stop brute‑force leaks on production servers
- Automating Let's‑Encrypt renewal on a single‑node web server without downtime
- When My Ubuntu 24.04 VPS Runs Out of Disk: A Step‑by‑Step Cleanup Script That Keeps Your Apache Logs from Failing
- When a VPS Disk Fills Mid‑Night: Quick Recovery with LVM Snapshots and Automated Alerts