Caddy reverse proxy Raspberry Pi gives you automatic HTTPS for every service on your home lab without managing certificates manually. Caddy uses the ACME protocol to obtain free Let’s Encrypt certificates and renews them automatically before expiry. A single Caddyfile routes traffic from public subdomains to local services like Nextcloud, Jellyfin, and Pi-hole, with TLS 1.3, HTTP/2, and OCSP stapling enabled by default. This guide covers installation on Bookworm, Caddyfile configuration for multiple services, DuckDNS dynamic DNS setup, security hardening, and troubleshooting.
Last tested: Raspberry Pi OS Bookworm Lite 64-bit | April 2, 2026 | Raspberry Pi 4 Model B (4GB) | Caddy 2.9.1 | DuckDNS | Nextcloud + Jellyfin + Pi-hole
Key Takeaways
- Caddy obtains and renews Let’s Encrypt certificates automatically using the ACME HTTP-01 or DNS-01 challenge. No Certbot, no cron jobs, no renewal scripts required.
- HTTPS redirection is automatic in Caddy. Adding a manual
redir http://rule to your Caddyfile is unnecessary and creates a conflict. - Subdomain-based routing (one subdomain per service) is more reliable than path-based routing. Many self-hosted apps expect to run at the root of a domain and break when served from a subpath.
- Use the official Caddy APT repository with a proper keyring file rather than
apt-key. Theapt-keymethod is deprecated in Bookworm. - Port 80 must remain accessible from the internet for the HTTP-01 ACME challenge. If your ISP blocks port 80, use the DNS-01 challenge instead (requires a DNS provider with API access).
- Test every Caddyfile change with
sudo caddy validate --config /etc/caddy/Caddyfilebefore reloading. A syntax error in a reload does not take down the running instance, but an error on startup does.

How Caddy Reverse Proxy Raspberry Pi Works
A reverse proxy sits between the public internet and your internal services. Caddy listens on ports 80 and 443, terminates TLS, and forwards decrypted requests to the appropriate local service based on the hostname in the request. Each service runs on its own port (Nextcloud on 8080, Jellyfin on 8096, etc.) and is never directly exposed to the internet. All services share a single public IP and a single set of ports.
Caddy’s automatic HTTPS means it contacts Let’s Encrypt during startup for any domain block that uses a public hostname, obtains a certificate, stores it locally, and begins serving HTTPS immediately. Renewals happen in the background without any service interruption.
| Feature | Caddy | Nginx | Traefik |
|---|---|---|---|
| Automatic HTTPS | Built-in, zero config | Manual (Certbot) | Built-in (with config) |
| Config format | Caddyfile (simple) | Nginx config (verbose) | YAML / TOML / labels |
| HTTP/2 | Automatic | Requires explicit config | Automatic |
| OCSP stapling | Automatic | Manual | Manual |
| Docker integration | Manual or plugin | Manual | Native label support |
| Pi CPU overhead | Low | Low | Medium |

Step 1: Prepare the Raspberry Pi
Flash Raspberry Pi OS Bookworm Lite 64-bit using Raspberry Pi Imager. In Imager’s advanced settings, set a hostname, enable SSH, and configure credentials. After first boot:
sudo apt update && sudo apt full-upgrade -y
sudo reboot
Set a static IP
Port forwarding on your router requires a stable LAN IP. Set one with nmcli on Bookworm:
nmcli connection show
sudo nmcli connection modify "Wired connection 1" \
ipv4.method manual \
ipv4.addresses 192.168.1.20/24 \
ipv4.gateway 192.168.1.1 \
ipv4.dns 192.168.1.1
sudo nmcli connection up "Wired connection 1"
On your router, forward ports 80 and 443 (TCP) to the Pi’s static IP. Port 80 is required for the ACME HTTP-01 challenge. Port 443 carries all HTTPS traffic.
Expected result: ip addr show eth0 shows the static IP. The Pi is reachable at that address after a reboot.
Step 2: Install Caddy
Install Caddy using the official APT repository. The correct method on Bookworm uses a keyring file rather than the deprecated apt-key:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
# Download and install the Caddy signing key
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /etc/apt/keyrings/caddy.gpg
# Add the Caddy repository
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 caddy -y
caddy version
The package installs a systemd service automatically. Enable and start it:
sudo systemctl enable caddy
sudo systemctl start caddy
sudo systemctl status caddy
Expected result: caddy version returns 2.8.x or higher. systemctl status caddy shows the service as active. Navigating to the Pi’s IP in a browser shows Caddy’s default page.
Step 3: Configure DuckDNS
Most home connections have a dynamic public IP. DuckDNS provides a free subdomain that updates to point to your current IP, giving Caddy a stable hostname for certificate issuance.
Sign up at duckdns.org, create a subdomain, and copy your token. Create the update script:
mkdir -p ~/duckdns
cat > ~/duckdns/duck.sh << 'EOF'
#!/bin/bash
echo url="https://www.duckdns.org/update?domains=YOURDOMAIN&token=YOURTOKEN&ip=" \
| curl -s -o ~/duckdns/duck.log -K -
EOF
chmod 700 ~/duckdns/duck.sh
Replace YOURDOMAIN and YOURTOKEN with your actual values. Add it to cron to run every 5 minutes:
crontab -e
# Add this line:
*/5 * * * * ~/duckdns/duck.sh >/dev/null 2>&1
Test the script manually first:
~/duckdns/duck.sh
cat ~/duckdns/duck.log
# Should return: OK
Verify DNS resolution from the Pi:
dig yourdomain.duckdns.org +short
# Should return your public IP
Expected result: cat ~/duckdns/duck.log returns OK. dig resolves to your public IP. DNS propagation can take a few minutes on first run.
Step 4: Write the Caddyfile
The Caddyfile lives at /etc/caddy/Caddyfile. Each domain block handles one service. Caddy obtains a certificate for each hostname automatically on first request.
Single service
yourdomain.duckdns.org {
reverse_proxy localhost:3000
}
Multiple subdomains
nextcloud.yourdomain.duckdns.org {
reverse_proxy 127.0.0.1:8080
}
jellyfin.yourdomain.duckdns.org {
reverse_proxy 127.0.0.1:8096
}
pihole.yourdomain.duckdns.org {
reverse_proxy 127.0.0.1:8081
}
Each subdomain requires a separate CNAME or A record in DuckDNS. If your DuckDNS plan only covers one subdomain, create additional subdomains in the DuckDNS dashboard and update each with the same script, passing the subdomain name as an argument.
Path-based routing (single domain)
Path-based routing works for simple services but breaks apps that expect to live at the domain root. Use it only when you cannot create subdomains:
yourdomain.duckdns.org {
handle_path /jellyfin/* {
reverse_proxy 127.0.0.1:8096
}
handle_path /pihole/* {
reverse_proxy 127.0.0.1:8081
}
}
Useful Caddyfile directives
| Directive | Effect | Example |
|---|---|---|
reverse_proxy | Forward requests to a backend | reverse_proxy 127.0.0.1:8080 |
encode gzip | Compress responses | encode gzip |
tls email@example.com | Set ACME notification address | tls you@example.com |
log | Enable structured access logging | log { output file /var/log/caddy/access.log } |
basicauth | Password-protect a block | See security section below |
header | Add or modify response headers | header X-Frame-Options DENY |
Validate and reload
# Validate syntax before applying
sudo caddy validate --config /etc/caddy/Caddyfile
# Reload without downtime
sudo systemctl reload caddy
Expected result: caddy validate returns no errors. After reload, navigating to your DuckDNS subdomain over HTTPS loads the proxied service with a valid Let’s Encrypt certificate.
Step 5: Docker Service Integration
When services run in Docker containers on a shared network, Caddy can reference them by container name rather than IP address. Create a Docker network for Caddy and your services:
docker network create caddy-net
In your service’s docker-compose.yml, add the network:
services:
nextcloud:
image: nextcloud
networks:
- caddy-net
networks:
caddy-net:
external: true
In the Caddyfile, reference the container by name:
nextcloud.yourdomain.duckdns.org {
reverse_proxy nextcloud:80
}
For Docker-based Caddy itself, mount the Caddyfile and certificate storage as volumes and connect to the same network. See the Traefik on Raspberry Pi article for a comparison of Docker-native reverse proxy approaches.
Step 6: Security Hardening
Firewall
sudo apt install ufw fail2ban -y
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Basic authentication for unprotected services
Services that lack their own login page (Pi-hole admin, some dashboards) can be protected with Caddy’s built-in basic auth. Generate a hashed password first:
caddy hash-password
# Enter your password at the prompt
# Copy the hash output -- it starts with $2a$
Add to the service block in the Caddyfile:
pihole.yourdomain.duckdns.org {
basicauth * {
admin <PASTE_HASH_HERE>
}
reverse_proxy 127.0.0.1:8081
}
Replace admin with the username and <PASTE_HASH_HERE> with the full hash string from caddy hash-password.
Security headers
nextcloud.yourdomain.duckdns.org {
reverse_proxy 127.0.0.1:8080
header {
Strict-Transport-Security "max-age=31536000;"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
Monitoring and Maintenance
# Follow live Caddy logs
sudo journalctl -u caddy -f
# Check certificate status
sudo caddy certificates
# View access logs (if configured in Caddyfile)
sudo tail -f /var/log/caddy/access.log
Caddy stores certificates under /var/lib/caddy/.local/share/caddy/certificates/. Back this directory up alongside your Caddyfile. Losing the certificate storage does not cause data loss. Caddy re-issues certificates on next startup, but repeated re-issuance triggers ACME rate limit consideration.
Keep Caddy updated. Security patches in the Caddy binary apply to all hosted services simultaneously:
sudo apt update && sudo apt upgrade caddy -y
sudo systemctl reload caddy
For backup of Pi configuration including the Caddyfile, see BorgBackup Raspberry Pi Prune Policies. For SD card write reduction on long-running installs, see Setting Up zram on Raspberry Pi.
Troubleshooting
Certificate not issuing
# Check logs for ACME error details
sudo journalctl -u caddy -n 100 | grep -i acme
# Verify DNS resolves to your public IP
dig yourdomain.duckdns.org +short
# Confirm port 80 is reachable from outside
curl http://yourdomain.duckdns.org
The most common causes are: port 80 not forwarded on the router, the DuckDNS script not updating (check duck.log), or the domain not resolving to the current public IP. Caddy retries certificate issuance automatically. Fix the underlying cause and it succeeds on the next attempt.
Service unreachable through proxy
# Confirm the backend service is running
curl -s http://127.0.0.1:8080 | head -5
# Check Caddy is forwarding to the correct port
sudo caddy validate --config /etc/caddy/Caddyfile
# Confirm Caddy can reach Docker container by name (if using Docker)
docker network inspect caddy-net
Port conflict on startup
# Find what is using port 80 or 443
sudo lsof -i :80
sudo lsof -i :443
# Apache is the most common conflict on a fresh Bookworm install
sudo systemctl stop apache2
sudo systemctl disable apache2
CGNAT blocking external access
Some ISPs use Carrier-Grade NAT (CGNAT), which means your router’s WAN IP is not a public IP. Port forwarding will not work in this case. Options: request a static IP from your ISP, use Cloudflare Tunnel (free tier handles this transparently), or set up a WireGuard tunnel to a VPS with a public IP. See WireGuard Raspberry Pi Site-to-Site VPN for the VPN approach.
Caddyfile validation errors
sudo caddy validate --config /etc/caddy/Caddyfile
Caddy’s error messages are precise about line number and directive. Common causes: mismatched braces, incorrect basicauth block syntax (must use basicauth * { username hash }), or a reverse_proxy pointing to a host that does not exist. Fix the error, validate again, then reload.
FAQ
Can I run Caddy and other services on the same Raspberry Pi?
Yes. Caddy itself uses minimal RAM (typically under 50MB at rest). Running Nextcloud, Jellyfin, Pi-hole, and Caddy simultaneously is well within the capacity of a Pi 4 with 4GB RAM. Use Docker to isolate services and prevent dependency conflicts.
What if my IP address changes frequently?
The DuckDNS cron script runs every 5 minutes and updates the DNS record when the IP changes. Certificate issuance is not affected by IP changes after the initial certificate is obtained. Caddy only needs DNS to point to the Pi at the moment of ACME challenge. Once the certificate is issued, Caddy serves HTTPS regardless of IP changes until renewal.
Do I need to open ports on my router?
Yes. Forward ports 80 and 443 (TCP) to the Pi’s static LAN IP. Port 80 is required for the ACME HTTP-01 challenge during certificate issuance and renewal. If your ISP blocks port 80, switch to the DNS-01 challenge in the Caddyfile using a DNS provider with API access (Cloudflare, Duck DNS via the Caddy DNS module).
Is Caddy better than Nginx for Raspberry Pi?
For self-hosted services requiring automatic HTTPS with minimal ongoing maintenance, Caddy requires significantly less configuration. Nginx is more configurable for complex routing scenarios and has broader documentation across the wider web. For a home lab with a handful of services, Caddy’s automatic certificate management and simpler syntax are practical advantages.
How often does Caddy renew certificates?
Caddy begins the renewal process when a certificate has 30 days remaining. Let’s Encrypt certificates are valid for 90 days, so renewals occur approximately every 60 days. The process runs in the background without any service interruption or manual action.
References
- https://caddyserver.com/docs/
- https://caddyserver.com/docs/caddyfile
- https://www.duckdns.org/install.jsp
- https://letsencrypt.org/docs/challenge-types/
About the Author
Chuck Wilson has been programming and building with computers since the Tandy 1000 era. His professional background includes CAD drafting, manufacturing line programming, and custom computer design. He runs PidiyLab in retirement, documenting Raspberry Pi and homelab projects that he actually deploys and maintains on real hardware. Every article on this site reflects hands-on testing on specific hardware and OS versions, not theoretical walkthroughs.
Last tested hardware: Raspberry Pi 4 Model B (4GB). Last tested OS: Raspberry Pi OS Bookworm Lite 64-bit. Caddy 2.9.1, DuckDNS, Nextcloud AIO, Jellyfin, Pi-hole.

