Caddy Reverse Proxy Raspberry Pi: Complete Setup Guide

Caddy Reverse Proxy Raspberry Pi Setup

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. The apt-key method 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/Caddyfile before reloading. A syntax error in a reload does not take down the running instance, but an error on startup does.
Caddy reverse proxy Raspberry Pi diagram showing public HTTPS requests routed through Caddy to Nextcloud Jellyfin and Pi-hole on local ports

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.

FeatureCaddyNginxTraefik
Automatic HTTPSBuilt-in, zero configManual (Certbot)Built-in (with config)
Config formatCaddyfile (simple)Nginx config (verbose)YAML / TOML / labels
HTTP/2AutomaticRequires explicit configAutomatic
OCSP staplingAutomaticManualManual
Docker integrationManual or pluginManualNative label support
Pi CPU overheadLowLowMedium
Caddy diagram cert

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

DirectiveEffectExample
reverse_proxyForward requests to a backendreverse_proxy 127.0.0.1:8080
encode gzipCompress responsesencode gzip
tls email@example.comSet ACME notification addresstls you@example.com
logEnable structured access logginglog { output file /var/log/caddy/access.log }
basicauthPassword-protect a blockSee security section below
headerAdd or modify response headersheader 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


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.