Caddy Reverse Proxy Setup for Self-Hosting

What Is Caddy?

Caddy is a web server and reverse proxy written in Go. Its defining feature is automatic HTTPS — every site you configure gets a valid SSL certificate from Let’s Encrypt with zero additional configuration. No certificate commands, no renewal cron jobs, no certificate files to manage.

Caddy’s configuration format (the Caddyfile) is dramatically simpler than Nginx config or Traefik labels. A complete reverse proxy rule for a service is two lines. This makes Caddy the fastest reverse proxy to get running for self-hosting.

For the conceptual overview of reverse proxies, see Reverse Proxy Explained.

Prerequisites

  • A Linux server with Docker and Docker Compose installed — see Docker Compose Basics
  • A domain name with A records pointed at your server’s IP
  • Ports 80 and 443 available (not used by another reverse proxy)
  • Basic familiarity with Docker — see Docker Networking

Docker Compose Configuration

Create a directory for Caddy:

mkdir -p /opt/caddy
cd /opt/caddy

Create docker-compose.yml:

# /opt/caddy/docker-compose.yml
services:
  caddy:
    image: caddy:2.9.1-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"  # HTTP/3 support
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data        # SSL certificates
      - caddy-config:/config    # Caddy configuration state
    networks:
      - caddy-public

volumes:
  caddy-data:
  caddy-config:

networks:
  caddy-public:
    name: caddy-public
    driver: bridge

The 443:443/udp port mapping enables HTTP/3 (QUIC), which Caddy supports natively.

The Caddyfile

Create a Caddyfile in the same directory:

# /opt/caddy/Caddyfile
# Global options
{
    email [email protected]  # CHANGE THIS — used for Let's Encrypt
}

# Example: Reverse proxy to a service
jellyfin.yourdomain.com {
    reverse_proxy jellyfin:8096
}

immich.yourdomain.com {
    reverse_proxy immich-server:2283
}

# Example: Simple file server
files.yourdomain.com {
    root * /srv/files
    file_server browse
}

That’s the entire config. Each site block is the domain name followed by directives in braces. reverse_proxy takes the upstream service name and port. Caddy handles SSL certificates, HTTP-to-HTTPS redirect, and HSTS headers automatically.

Start Caddy

docker compose up -d

Check the logs to verify certificate provisioning:

docker compose logs -f caddy

You should see Caddy obtain SSL certificates for each configured domain. Access your services at their domains — HTTPS works immediately.

Adding Services

Method 1: Add to Caddyfile (Simple)

For each new service, add a block to the Caddyfile:

myapp.yourdomain.com {
    reverse_proxy myapp:3000
}

Reload Caddy without downtime:

docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

The service must be on the caddy-public Docker network. In the service’s docker-compose.yml:

services:
  myapp:
    image: myapp:v1.0.0
    restart: unless-stopped
    networks:
      - caddy-public

networks:
  caddy-public:
    external: true

Method 2: Use Caddy Docker Proxy Plugin

For Traefik-like automatic Docker label integration, use the caddy-docker-proxy image instead of stock Caddy:

services:
  caddy:
    image: lucaslorentz/caddy-docker-proxy:2.9.1
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - caddy-data:/data
    networks:
      - caddy-public

volumes:
  caddy-data:

networks:
  caddy-public:
    name: caddy-public
    driver: bridge

Then add labels to your services instead of editing the Caddyfile:

services:
  myapp:
    image: myapp:v1.0.0
    restart: unless-stopped
    networks:
      - caddy-public
    labels:
      caddy: myapp.yourdomain.com
      caddy.reverse_proxy: "{{upstreams 3000}}"

networks:
  caddy-public:
    external: true

Common Caddyfile Patterns

Basic Auth Protection

admin.yourdomain.com {
    basicauth {
        admin $2a$14$your_bcrypt_hash_here
    }
    reverse_proxy admin-panel:8080
}

Generate the password hash:

docker run --rm caddy:2.9.1-alpine caddy hash-password --plaintext 'your-password'

Websocket Support

Caddy proxies websockets automatically — no extra configuration needed. This means apps like Home Assistant, chat services, and real-time dashboards work out of the box.

Rate Limiting

api.yourdomain.com {
    rate_limit {
        zone api_limit {
            key {remote_host}
            events 100
            window 1m
        }
    }
    reverse_proxy api-service:8000
}

IP Restriction (Internal Services Only)

internal.yourdomain.com {
    @blocked not remote_ip 192.168.0.0/16 10.0.0.0/8
    respond @blocked "Forbidden" 403
    reverse_proxy internal-app:3000
}

Multiple Services on Subpaths

yourdomain.com {
    handle /jellyfin/* {
        reverse_proxy jellyfin:8096
    }
    handle /gitea/* {
        reverse_proxy gitea:3000
    }
    handle {
        reverse_proxy homepage:3000
    }
}

Security Headers

yourdomain.com {
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
    reverse_proxy myapp:8080
}

Wildcard Certificates

For wildcard certificates (*.yourdomain.com), Caddy needs DNS challenge verification. Using Cloudflare:

Build a custom Caddy image with the Cloudflare DNS plugin:

FROM caddy:2.9.1-builder AS builder
RUN xcaddy build \
    --with github.com/caddy-dns/cloudflare

FROM caddy:2.9.1-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

Build and use this image:

docker build -t caddy-cloudflare .

Update docker-compose.yml to use caddy-cloudflare and add your API token:

services:
  caddy:
    build: .
    environment:
      - CLOUDFLARE_API_TOKEN=your_token_here

Update the Caddyfile:

*.yourdomain.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }

    @jellyfin host jellyfin.yourdomain.com
    handle @jellyfin {
        reverse_proxy jellyfin:8096
    }

    @immich host immich.yourdomain.com
    handle @immich {
        reverse_proxy immich-server:2283
    }
}

Caddy vs Nginx Proxy Manager vs Traefik

FeatureCaddyNginx Proxy ManagerTraefik
ConfigurationCaddyfile (text)Web GUIDocker labels / YAML
SSL certificatesAutomaticAutomaticAutomatic
Learning curveLowVery LowMedium
HTTP/3Yes (native)NoYes
Docker integrationManual / pluginManualNative
Config reloadZero downtimeRestart requiredAutomatic
Best forSimple setups, clean configsBeginnersDocker-heavy infra

Use Caddy if you want the simplest possible configuration with excellent defaults. Use Nginx Proxy Manager if you want a GUI. Use Traefik if you need native Docker label integration.

Common Mistakes

Forgetting to Expose the Service Network

The most common issue: Caddy can’t reach your service. Both Caddy and the service must be on the same Docker network. Verify with:

docker network inspect caddy-public

Look for both containers in the output.

Using IP Addresses Instead of Container Names

Inside Docker networks, use container names (or service names) as hostnames, not IP addresses. Container IPs can change on restart.

# Wrong
reverse_proxy 172.18.0.5:8096

# Correct
reverse_proxy jellyfin:8096

Running Caddy as Non-Root and Binding Port 80

If you run Caddy outside Docker as a non-root user, it can’t bind to ports below 1024. Either use Docker (recommended), or grant the binary the cap_net_bind_service capability:

sudo setcap cap_net_bind_service=+ep $(which caddy)

DNS Not Propagated

If Caddy fails to get a certificate, verify your DNS A record is pointing to the correct IP and has propagated:

dig +short myapp.yourdomain.com

Let’s Encrypt needs to reach your server on port 80 for HTTP challenge verification.

Next Steps

FAQ

Is Caddy fast enough for production use?

Yes. Caddy handles thousands of concurrent connections efficiently. For a home server with under 100 concurrent users, performance is not a concern. Caddy also supports HTTP/3 natively, which can improve performance for mobile clients.

Can I migrate from Nginx Proxy Manager to Caddy?

Yes. The process is straightforward — for each proxy host in NPM, create a corresponding site block in the Caddyfile. Caddy provisions new certificates automatically. Stop NPM, start Caddy, and your services route to the same backends.

Does Caddy cache content?

Not by default. Caddy is a reverse proxy — it forwards requests to your backend services. For caching, your backend application should handle it, or you can add the cache-handler plugin to Caddy.

How do I update Caddy?

Change the image tag in docker-compose.yml (e.g., caddy:2.9.2-alpine) and run docker compose up -d. Certificates and configuration persist in the mounted volumes. See Updating Docker Containers.