Caddy Reverse Proxy: HTTPS Without the Headache

Discover why Caddy's automatic HTTPS and simple Caddyfile syntax make it the easiest reverse proxy for homelab setups. No Certbot, no manual certificate management—just write your domain and go.

• 10 min read
self-hostedreverse-proxycaddydockerhomelabhttps
Caddy Reverse Proxy: HTTPS Without the Headache

If you’ve ever spent an afternoon wrestling with Nginx config files, debugging why Let’s Encrypt certificates won’t renew, or trying to remember which port needs to be forwarded… Caddy is here to end that suffering. It’s the reverse proxy that just works—automatic HTTPS included.

The Certificate Management Problem

Every self-hoster knows the dance:

  1. Set up Nginx or Apache
  2. Install Certbot
  3. Run Certbot with the right flags
  4. Set up a cron job for renewal
  5. Debug when renewal silently fails six months later
  6. Wonder why your site shows security warnings
Warning

Certificate expiration is one of the most common causes of website downtime. Let’s Encrypt certificates only last 90 days—if renewal fails, your site breaks silently.

Caddy eliminates this entire workflow. It obtains, installs, and renews TLS certificates automatically. You don’t think about certificates. You don’t configure cron jobs. You just… write your domain name.

What Makes Caddy Different

Caddy was created by Matthew Holt in 2014 with a simple but revolutionary idea: a web server that uses HTTPS by default. Not “HTTPS available” or “HTTPS with configuration”—HTTPS is the baseline behavior.

Here’s what sets it apart from Traefik and Nginx:

FeatureCaddyTraefikNginx
Automatic HTTPS✅ Built-in, zero config✅ Built-in, needs config❌ Manual (Certbot)
Config FormatCaddyfile (intuitive)YAML/TOML (verbose)nginx.conf (complex)
Learning CurveLowMediumHigh
HTTP/3✅ On by default✅ Available❌ Manual compile
DNS ChallengeModule neededBuilt-in providersManual setup
Docker Labels❌ No✅ Primary method❌ No
Static Binary✅ Yes❌ No✅ Yes
Pro Tip

If you prefer Docker labels and have a dynamic container fleet that changes frequently, Traefik’s label-based discovery might suit you better. But for simpler setups, Caddy’s Caddyfile approach is significantly more readable.

The Caddyfile: Configuration That Humans Can Read

Caddy uses a configuration format called the Caddyfile. It’s designed to be read and written by humans. Compare:

Nginx:

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;
    
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Caddy:

example.com {
    reverse_proxy localhost:8080
}

That’s it. Three lines. Caddy handles:

  • HTTP → HTTPS redirect
  • TLS certificate provisioning
  • Certificate renewal
  • HTTP/2 and HTTP/3
  • Proper headers

Getting Started: Docker Compose Setup

Let’s set up Caddy the right way. Create the directory structure:

mkdir -p ~/caddy/{data,config}
cd ~/caddy
touch Caddyfile

Here’s a production-ready docker-compose.yml:

version: "3.8"

services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"  # HTTP/3 support
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./data:/data
      - ./config:/config
    environment:
      # Uncomment if using Cloudflare DNS challenge
      # - CF_API_TOKEN=your-cloudflare-api-token
    
  # Example backend service
  app:
    image: nginx:alpine
    restart: unless-stopped
    # No labels needed! Just reference in Caddyfile
Pro Tip

The :ro flag on Caddyfile makes it read-only in the container—you don’t want Caddy modifying your config.

Your First Reverse Proxy

Create a Caddyfile in the same directory:

yourdomain.com {
    reverse_proxy app:80
}

Run it:

docker compose up -d

Caddy will:

  1. Detect yourdomain.com needs HTTPS
  2. Request a certificate from Let’s Encrypt
  3. Set up automatic HTTP → HTTPS redirect
  4. Enable HTTP/2 and HTTP/3
  5. Proxy requests to your app container

No certificate management. No manual port configuration. It just works.

Behind the Magic: Caddy’s Architecture

Caddy doesn’t use configuration files the way Nginx does. Its architecture is fundamentally different:

┌─────────────────────────────────────────────────────────────────┐
│                         CADDY SERVER                            │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    CERTMAGIC (Automatic TLS)              │  │
│  │  • Let's Encrypt & ZeroSSL (with automatic failover)     │  │
│  │  • Certificate caching and renewal                       │  │
│  │  • On-demand TLS for dynamic domains                     │  │
│  └──────────────────────────────────────────────────────────┘  │
│                              │                                  │
│                              ▼                                  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    HTTP SERVER                            │  │
│  │  • HTTP/1.1, HTTP/2, HTTP/3 (QUIC)                        │  │
│  │  • Request routing and matching                          │  │
│  │  • Middleware chain execution                             │  │
│  └──────────────────────────────────────────────────────────┘  │
│                              │                                  │
│                              ▼                                  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    HANDLERS                                │  │
│  │  • reverse_proxy  • file_server  • respond                │  │
│  │  • encode         • templates    • php_fastcgi            │  │
│  └──────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

CertMagic: The Secret Sauce

Caddy’s automatic HTTPS is powered by CertMagic, a separate Go library Matthew Holt developed. It handles:

  • Multi-issuer fallback: Try Let’s Encrypt, fall back to ZeroSSL automatically
  • Background renewal: Certificates are renewed 30 days before expiration
  • Error recovery: Network issues? Caddy retries with exponential backoff
  • Rate limiting: Built-in throttle to avoid CA rate limits
Note

CertMagic is available as a standalone Go library. You can use it in any Go project for automatic certificate management.

Caddyfile Directives

Caddyfile has a simple but powerful directive system. Here are the essential ones:

reverse_proxy

The workhorse. Proxies requests to backend services:

app.example.com {
    reverse_proxy localhost:8080
}

With health checks:

app.example.com {
    reverse_proxy {
        to app1:8080
        to app2:8080
        
        health_uri /health
        health_interval 30s
    }
}

file_server

Serve static files:

example.com {
    root * /var/www/html
    file_server
}

With browsing enabled:

files.example.com {
    root * /var/www/files
    file_server browse
}

encode

Enable compression:

example.com {
    encode gzip zstd
    reverse_proxy localhost:8080
}

handle

Conditional routing:

example.com {
    handle /api/* {
        reverse_proxy localhost:8000
    }
    
    handle {
        root * /var/www/html
        file_server
    }
}

Basic Authentication

Protect routes:

admin.example.com {
    basicauth {
        admin $2a$14$hashedpassword
    }
    reverse_proxy localhost:8080
}

Generate passwords with Caddy’s built-in command:

caddy hash-password --plaintext "yourpassword"

Path Matching and Subpaths

Caddy’s matchers let you route based on paths, headers, and more:

example.com {
    # API goes to backend
    reverse_proxy /api/* localhost:8000
    
    # WebSocket connections
    reverse_proxy /ws/* localhost:3000
    
    # Everything else: static files
    root * /var/www/html
    file_server
}

Named Matchers

For complex matching logic:

example.com {
    @api {
        path /api/*
        method GET POST
    }
    
    reverse_proxy @api localhost:8000 {
        header_up X-Custom-Header "value"
    }
}

Wildcards and DNS Challenge

For wildcard certificates like *.example.com, you need the DNS challenge. This requires building Caddy with a DNS provider module.

Building Caddy with xcaddy

# Install xcaddy
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

# Build with Cloudflare DNS module
xcaddy build --with github.com/caddy-dns/cloudflare

# Or use Docker
docker build -t caddy-cloudflare - <<'EOF'
FROM caddy:builder AS builder
RUN xcaddy build --with github.com/caddy-dns/cloudflare
FROM caddy:latest
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
EOF

Cloudflare Configuration

Create an API token with these permissions:

  • Zone.Zone:Read
  • Zone.DNS:Edit

Then configure Caddyfile:

{
    email [email protected]
}

*.example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    
    @app host app.example.com
    handle @app {
        reverse_proxy localhost:8080
    }
    
    @api host api.example.com
    handle @api {
        reverse_proxy localhost:8000
    }
}
Warning

DNS challenge is required for wildcard certificates. HTTP-01 and TLS-ALPN-01 challenges can’t validate wildcards.

Real-World Homelab Configuration

Here’s a complete Caddyfile for a typical homelab stack:

{
    # Global options
    email [email protected]
    acme_ca https://acme-v02.api.letsencrypt.org/directory
    
    # For internal services behind VPN
    # local_certs
}

# Photo backup - Immich
photos.example.com {
    reverse_proxy localhost:2283
}

# Media server - Jellyfin
media.example.com {
    reverse_proxy localhost:8096 {
        # Required for Jellyfin WebSocket connections
        header_up Host {host}
    }
}

# Home Assistant
home.example.com {
    reverse_proxy localhost:8123
}

# Docker management - Portainer
portainer.example.com {
    reverse_proxy localhost:9443 {
        transport http {
            # Portainer uses self-signed certs
            tls_insecure_skip_verify
        }
    }
}

# Document manager - Paperless
docs.example.com {
    reverse_proxy localhost:8000
}

# Password manager - Vaultwarden
vault.example.com {
    reverse_proxy localhost:3012 {
        header_up X-Real-IP {remote_host}
    }
}

# Monitoring - Uptime Kuma
status.example.com {
    reverse_proxy localhost:3001
}
Pro Tip

For internal services that shouldn’t be exposed publicly, use Tailscale or a VPN. Caddy can also generate local CA certificates for .local domains if you use the local_certs option.

Proxmox Integration

Running services on Proxmox? Caddy works great in an LXC container or VM:

Option 1: Dedicated Caddy LXC

# Create privileged LXC on Proxmox
# Then install Caddy directly:

apt update
apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' > /etc/apt/sources.list.d/caddy-stable.list
apt update
apt install caddy

Option 2: Docker in LXC

Run Docker inside a privileged LXC, then use the Docker Compose setup from earlier.

Option 3: Cloudflare Tunnel

For homelabs behind CGNAT or without public IP:

# Use Cloudflare Tunnel for external access
# Caddy only handles internal routing
*.lan.example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    
    # Internal services on local network
    @photos host photos.lan.example.com
    handle @photos {
        reverse_proxy 192.168.1.100:2283
    }
}

Caddy vs Tailscale Funnel

If you’re using Tailscale for VPN access, you might consider Tailscale Funnel for exposing services. Here’s the trade-off:

FeatureCaddy + DNS ChallengeTailscale Funnel
Setup complexityMediumLow
Custom domains✅ Full control❌ ts.net only
Performance✅ Direct❌ Relayed through Tailscale
Privacy✅ Your server❌ Through Tailscale infra
CostFree (Let’s Encrypt)Free tier limits
Best forPermanent servicesQuick testing, internal demos
Pro Tip

Use Caddy for production services you want accessible long-term. Use Tailscale Funnel for quick demos or internal-only services.

Advanced: Multiple Upstreams and Load Balancing

Caddy supports multiple backends with automatic health checking:

app.example.com {
    reverse_proxy {
        to app1:8080
        to app2:8080
        to app3:8080
        
        # Health checks
        health_uri /health
        health_interval 30s
        health_timeout 5s
        
        # Retry failed requests
        lb_try_duration 5s
        
        # Load balancing: random (default), first, round_robin, least_conn
        lb_policy least_conn
    }
}

Performance and Observability

Metrics Endpoint

Enable Prometheus metrics:

{
    servers {
        metrics
    }
}

app.example.com {
    reverse_proxy localhost:8080
}

Access metrics at http://localhost:2019/metrics.

Admin API

Caddy exposes an admin API on port 2019 by default:

# View current config
curl localhost:2019/config/

# Reload configuration
curl -X POST localhost:2019/load -H "Content-Type: application/json" -d @config.json

# List certificates
curl localhost:2019/certificates
Warning

The admin API should never be exposed publicly. Bind to localhost only or use authentication.

Performance Benchmarks

Caddy is highly performant—roughly comparable to Nginx in most workloads:

  • Static files: ~150,000 req/s on modern hardware
  • Reverse proxy: ~120,000 req/s (depends on backend)
  • Memory overhead: ~15-30MB baseline
  • Latency: Sub-millisecond added latency

For most homelab setups, Caddy will never be your bottleneck.

When to Choose Caddy

Choose Caddy if:

  • You want zero-config HTTPS
  • You prefer simple config files over labels
  • You’re running services on bare metal, LXC, or a small number of Docker containers
  • You want HTTP/3 enabled by default with no extra work
  • You’re tired of certificate management

Consider Traefik instead if:

  • You have a large, dynamic Docker Swarm or Kubernetes cluster
  • You rely heavily on container labels for configuration
  • You need Kubernetes ingress controller features

Stick with Nginx if:

  • You have complex caching requirements
  • You need advanced load balancing features (though Caddy covers most cases)
  • You’re already deeply invested in Nginx expertise

Troubleshooting Common Issues

Certificates Not Obtaining

# Check Caddy logs
docker logs caddy

# Common causes:
# 1. DNS not pointing to your server
# 2. Firewall blocking port 80/443
# 3. Rate limited (wait 1 hour, use staging)

DNS Challenge Failing

# Verify API token works
curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
  -H "Authorization: Bearer YOUR_TOKEN"

# Check DNS propagation
dig @1.1.1.1 _acme-challenge.example.com TXT

Permission Denied on Ports 80/443

# Allow Caddy to bind low ports
sudo setcap cap_net_bind_service=+ep $(which caddy)

Conclusion: The Simplicity Dividend

Caddy’s philosophy is that modern web serving shouldn’t require a certification to configure. HTTPS should be automatic. Config files should be readable. Renewal should be invisible.

For homelab setups, this simplicity dividend compounds:

  • No more certificate fires: Set it and forget it
  • No more config file dread: The Caddyfile is genuinely easy to read
  • No more “why is this broken?”: Fewer moving parts = fewer failures
  • More time for what matters: Your actual services

If Traefik is the Docker-native choice, Caddy is the human-native choice. Both are excellent—but if certificate management has ever caused you pain, Caddy is worth your attention.

Quick Reference

# Install
docker pull caddy:latest

# Basic Caddyfile
example.com {
    reverse_proxy localhost:8080
}

# Validate config
caddy validate --config Caddyfile

# Reload config
caddy reload --config Caddyfile

# Format Caddyfile
caddy fmt --overwrite Caddyfile

# View certificates
curl localhost:2019/certificates

Resources:

Anthony Lattanzio

Anthony Lattanzio

Tech Enthusiast & Builder

I'm a tech enthusiast who loves building things with hardware and software. By night, I run a homelab that's grown way beyond what any reasonable person needs. Check out about me for more.

Comments

Powered by GitHub Discussions