---
# === IDENTITY ===
id: software/devops/caddy-server-config/2026
canonical_question: "Caddy server configuration reference"
aliases:
  - "Caddyfile configuration guide"
  - "Caddy automatic HTTPS reverse proxy setup"
  - "How to configure Caddy web server"
  - "Caddy vs nginx configuration"
entity_type: software_reference
domain: software > devops > Caddy Server Config
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-28
confidence: 0.92
version: 1.0
first_published: 2026-02-28

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "Caddy v2.0 (2020) — complete rewrite from v1, Caddyfile syntax changed"
  next_review: 2026-08-27
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "Caddy must be able to bind to ports 80 and 443 for automatic HTTPS — use setcap on Linux if not running as root"
  - "DNS must point to the server BEFORE enabling automatic HTTPS — otherwise ACME challenge fails"
  - "Caddy stores certificates in /data directory — must be persistent (mount as volume in Docker)"
  - "Do not set lb_try_interval to 0 with non-zero lb_try_duration — causes CPU spin"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "You need fine-grained control over every header and buffer setting"
    use_instead: "software/devops/nginx-common-configs/2026"
  - condition: "You need a serverless edge proxy"
    use_instead: "software/devops/cloudflare-workers-setup/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "use_case"
    question: "What is the primary use case for Caddy?"
    type: choice
    options: ["Reverse proxy with auto-HTTPS", "Static file server", "Load balancer", "PHP/FastCGI server"]
  - key: "deployment"
    question: "How are you deploying Caddy?"
    type: choice
    options: ["Bare metal / VM", "Docker", "Kubernetes"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/devops/caddy-server-config/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-28)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to: []
  solves: []
  alternative_to:
    - id: software/devops/nginx-common-configs/2026
      label: "nginx.conf Reference for Common Scenarios"
  often_confused_with: []

# === SOURCES ===
sources:
  - id: src1
    title: "Caddyfile Concepts"
    author: Caddy
    url: https://caddyserver.com/docs/caddyfile/concepts
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src2
    title: "reverse_proxy Directive"
    author: Caddy
    url: https://caddyserver.com/docs/caddyfile/directives/reverse_proxy
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src3
    title: "HTTPS Quick-Start"
    author: Caddy
    url: https://caddyserver.com/docs/quick-starts/https
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src4
    title: "Common Caddyfile Patterns"
    author: Caddy
    url: https://caddyserver.com/docs/caddyfile/patterns
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src5
    title: "Global Options Reference"
    author: Caddy
    url: https://caddyserver.com/docs/caddyfile/options
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src6
    title: "Caddy Features Overview"
    author: Caddy
    url: https://caddyserver.com/features
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src7
    title: "Reverse Proxy Quick-Start"
    author: Caddy
    url: https://caddyserver.com/docs/quick-starts/reverse-proxy
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
---

# Caddy Server Configuration Reference

## TL;DR

- **Bottom line**: Caddy is a modern web server that provides automatic HTTPS out of the box — a production-ready Caddyfile is typically 15-25% the size of an equivalent nginx config.
- **Key tool/command**: `caddy run` (foreground) or `caddy start` (background), `caddy reload` to apply changes
- **Watch out for**: DNS must point to your server BEFORE Caddy tries to obtain certificates — otherwise the ACME challenge fails and your site won't start.
- **Works with**: Caddy 2.8+, Let's Encrypt / ZeroSSL (automatic), any backend (HTTP, gRPC, WebSocket, FastCGI).

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Caddy needs to bind ports 80 and 443 for automatic HTTPS — on Linux, use `setcap cap_net_bind_service=+ep /usr/bin/caddy` if not root.
- DNS must resolve to the server before enabling HTTPS — ACME HTTP-01 challenge requires port 80 access.
- The `/data` directory stores certificates and must be persistent — in Docker, mount as a volume.
- The `/config` directory stores the active config — also mount in Docker.
- Do not set `lb_try_interval 0` with non-zero `lb_try_duration` — causes CPU spin if all backends are down.

## Quick Reference

| Directive | Purpose | Example |
|---|---|---|
| `reverse_proxy` | Proxy to backend | `reverse_proxy localhost:3000` |
| `file_server` | Serve static files | `file_server` or `file_server browse` |
| `root` | Set document root | `root * /var/www/html` |
| `tls` | Configure TLS/HTTPS | `tls admin@example.com` |
| `encode` | Enable compression | `encode gzip zstd` |
| `header` | Set response headers | `header X-Frame-Options DENY` |
| `log` | Configure access logs | `log { output file /var/log/caddy/access.log }` |
| `basicauth` | HTTP basic auth | `basicauth /admin/* { ... }` |
| `respond` | Static response | `respond "Hello" 200` |
| `redir` | HTTP redirect | `redir https://new.example.com{uri}` |
| `rewrite` | URL rewrite | `rewrite /old /new` |
| `handle` | Exclusive route group | `handle /api/* { ... }` |
| `handle_path` | Handle + strip prefix | `handle_path /api/* { ... }` |
| `import` | Include snippets | `import common_headers` |
| `lb_policy` | Load balancing algorithm | `lb_policy least_conn` |

## Decision Tree

```
START
├── Need automatic HTTPS with zero config?
│   ├── YES → Just use domain name as site address (auto-provisioned)
│   └── NO ↓
├── Reverse proxy to single backend?
│   ├── YES → One-line: `example.com { reverse_proxy localhost:3000 }`
│   └── NO ↓
├── Reverse proxy to multiple backends?
│   ├── YES → reverse_proxy with multiple upstreams + lb_policy
│   └── NO ↓
├── Serving static files?
│   ├── YES → `root * /path` + `file_server`
│   └── NO ↓
├── PHP application?
│   ├── YES → `php_fastcgi localhost:9000`
│   └── NO ↓
├── Need SPA (single-page app) routing?
│   ├── YES → `try_files {path} /index.html`
│   └── NO ↓
└── DEFAULT → reverse_proxy + file_server combo
```

## Step-by-Step Guide

### 1. Install Caddy

Install Caddy on your system. [src6]

```bash
# Ubuntu/Debian
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
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

# macOS
brew install caddy

# Docker
docker run -d -p 80:80 -p 443:443 \
  -v caddy_data:/data \
  -v caddy_config:/config \
  -v ./Caddyfile:/etc/caddy/Caddyfile \
  caddy:2
```

**Verify**: `caddy version` → `v2.8.x`

### 2. Create a basic Caddyfile

Write a minimal reverse proxy config. [src1] [src7]

```caddyfile
# /etc/caddy/Caddyfile

example.com {
    reverse_proxy localhost:3000
}
```

That is it. Caddy will automatically:
- Obtain a TLS certificate from Let's Encrypt
- Redirect HTTP to HTTPS
- Serve over HTTP/2 and HTTP/3
- Renew certificates before expiry

**Verify**: `caddy validate --config /etc/caddy/Caddyfile` → `Valid configuration`

### 3. Add static file serving alongside proxy

Serve static assets directly and proxy everything else. [src4]

```caddyfile
example.com {
    # Serve static files from /var/www/static
    handle /static/* {
        root * /var/www
        file_server
    }

    # Everything else goes to backend
    handle {
        reverse_proxy localhost:3000
    }

    # Compression
    encode gzip zstd

    # Security headers
    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        Strict-Transport-Security "max-age=63072000"
        -Server  # Remove Server header
    }
}
```

**Verify**: `caddy reload --config /etc/caddy/Caddyfile`

### 4. Configure load balancing

Distribute traffic across multiple backends. [src2]

```caddyfile
api.example.com {
    reverse_proxy {
        to localhost:3001
        to localhost:3002
        to localhost:3003

        lb_policy least_conn

        health_uri /health
        health_interval 10s
        health_timeout 5s
        health_status 200

        # Passive health checks
        fail_duration 30s
        max_fails 3
        unhealthy_latency 1s
    }
}
```

**Verify**: `caddy reload`, then check backends are receiving balanced traffic

### 5. Set up multi-site configuration

Host multiple sites in one Caddyfile. [src1] [src4]

```caddyfile
# Snippet for shared headers
(security_headers) {
    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
        Strict-Transport-Security "max-age=63072000; includeSubDomains"
        -Server
    }
}

app.example.com {
    import security_headers
    reverse_proxy localhost:3000
    encode gzip
    log {
        output file /var/log/caddy/app.log
        format json
    }
}

api.example.com {
    import security_headers
    reverse_proxy localhost:4000
    encode gzip

    # Rate limiting (requires caddy-ratelimit plugin)
    # rate_limit {remote.ip} 10r/s
}

static.example.com {
    import security_headers
    root * /var/www/static
    file_server
    encode gzip

    @immutable path *.js *.css *.woff2 *.png *.jpg
    header @immutable Cache-Control "public, max-age=31536000, immutable"
}
```

**Verify**: `caddy validate` then `caddy reload`

### 6. Docker Compose deployment

Deploy Caddy with Docker Compose. [src6]

```yaml
# docker-compose.yml
services:
  caddy:
    image: caddy:2
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"  # HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data    # Certificates
      - caddy_config:/config
    restart: unless-stopped

  app:
    image: node:22-slim
    command: node server.js
    expose:
      - "3000"

volumes:
  caddy_data:
  caddy_config:
```

```caddyfile
# Caddyfile for Docker
example.com {
    reverse_proxy app:3000
}
```

**Verify**: `docker compose up -d && docker compose logs caddy`

## Code Examples

### Single-page application (SPA) with API proxy

```caddyfile
# Input:  SPA (React/Vue/Angular) with backend API
# Output: Serves SPA files, proxies /api to backend

example.com {
    # API routes → backend
    handle_path /api/* {
        reverse_proxy localhost:4000
    }

    # SPA fallback — serve index.html for all routes
    handle {
        root * /var/www/spa
        try_files {path} /index.html
        file_server
    }

    encode gzip
    header {
        X-Frame-Options "DENY"
        X-Content-Type-Options "nosniff"
        -Server
    }
}
```

### PHP application with FastCGI

```caddyfile
# Input:  PHP application (Laravel, WordPress)
# Output: Caddy serving PHP via FastCGI

example.com {
    root * /var/www/html

    # PHP processing
    php_fastcgi localhost:9000

    # Static files
    file_server

    encode gzip

    # Block access to sensitive files
    @blocked path /.env /.git/* /composer.json /composer.lock
    respond @blocked 404
}
```

### Wildcard subdomain proxy

```caddyfile
# Input:  Wildcard subdomains (*.example.com)
# Output: Dynamic reverse proxy based on subdomain

*.example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }

    @app host app.example.com
    handle @app {
        reverse_proxy localhost:3000
    }

    @api host api.example.com
    handle @api {
        reverse_proxy localhost:4000
    }

    handle {
        respond "Unknown subdomain" 404
    }
}
```

## Anti-Patterns

### Wrong: Manually configuring TLS when automatic works

```caddyfile
# BAD — unnecessary manual TLS config when auto-HTTPS works
example.com {
    tls /etc/ssl/certs/example.com.pem /etc/ssl/private/example.com.key
    # Caddy does this automatically for public domains!
}
```

### Correct: Let Caddy handle HTTPS automatically

```caddyfile
# GOOD — Caddy obtains, manages, and renews certs automatically
example.com {
    reverse_proxy localhost:3000
}
```

### Wrong: Using IP address and expecting HTTPS

```caddyfile
# BAD — Caddy cannot obtain certs for bare IPs
192.168.1.100 {
    reverse_proxy localhost:3000
    # No HTTPS — ACME doesn't issue certs for IPs
}
```

### Correct: Use a domain name for automatic HTTPS

```caddyfile
# GOOD — domain name triggers automatic HTTPS
app.example.com {
    reverse_proxy localhost:3000
}
```

### Wrong: Not persisting /data in Docker

```yaml
# BAD — certificates lost on container restart
services:
  caddy:
    image: caddy:2
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
    # Missing /data and /config volumes!
```

### Correct: Mount /data and /config volumes

```yaml
# GOOD — certificates survive container restarts
services:
  caddy:
    image: caddy:2
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
volumes:
  caddy_data:
  caddy_config:
```

## Common Pitfalls

- **DNS not pointing to server before first run**: ACME challenge fails, Caddy won't start with HTTPS. Fix: verify DNS resolves correctly with `dig example.com` before starting Caddy. [src3]
- **Port 80 or 443 already in use**: Another web server (Apache, nginx) blocking Caddy. Fix: stop the other server or change Caddy's ports with `http_port` / `https_port` global options. [src5]
- **Forgetting Docker volumes for /data**: Certificates are re-requested on every restart, hitting Let's Encrypt rate limits. Fix: always mount `/data` as a named volume. [src6]
- **Using `handle` vs `handle_path`**: `handle` preserves the URI path, `handle_path` strips the matched prefix. Fix: use `handle_path /api/*` when your backend expects paths without the `/api` prefix. [src4]
- **Directive order matters**: Caddy has a default directive order (e.g., `encode` before `reverse_proxy`). Fix: if you need custom ordering, use `route { ... }` blocks. [src1]
- **Rate limit exceeding with Let's Encrypt**: Too many certificate requests (>50 per domain per week). Fix: use staging CA for testing: `tls { ca https://acme-staging-v02.api.letsencrypt.org/directory }`. [src3]

## Diagnostic Commands

```bash
# Validate Caddyfile syntax
caddy validate --config /etc/caddy/Caddyfile

# Start Caddy in foreground (see logs)
caddy run --config /etc/caddy/Caddyfile

# Reload config (zero-downtime)
caddy reload --config /etc/caddy/Caddyfile

# Show active configuration
caddy list-modules

# Check certificate status
caddy trust  # Install Caddy's root CA locally

# View Caddy's managed certificates
ls $HOME/.local/share/caddy/certificates/

# Test HTTPS
curl -I https://example.com

# Check if ports are available
ss -tlnp | grep -E ':(80|443)'
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Caddy 2.8.x | Current | None | Recommended version |
| Caddy 2.7.x | Supported | None | — |
| Caddy 2.6.x | Older | — | Some directive changes |
| Caddy 2.0 | GA (2020) | Complete rewrite from v1 | Caddyfile syntax incompatible with v1 |
| Caddy 1.x | EOL | — | Must migrate to v2 Caddyfile syntax |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Automatic HTTPS with zero config | Need granular control over every setting | nginx |
| Quick reverse proxy for internal services | High-performance static CDN (millions req/s) | nginx + CloudFront |
| Docker/K8s deployments needing easy config | Need extensive third-party module ecosystem | nginx |
| PHP hosting with built-in FastCGI | Need raw TCP/UDP proxying | HAProxy |
| Development HTTPS with local CA | Enterprise WAF requirements | nginx + ModSecurity |

## Important Caveats

- Caddy 2.x Caddyfile syntax is completely incompatible with Caddy 1.x — there is no automatic migration tool.
- Automatic HTTPS only works for publicly resolvable domain names — for internal/private domains, use `tls internal` to use Caddy's local CA.
- Caddy stores certificates in `$XDG_DATA_HOME/caddy` (or `/data` in Docker) — losing this directory forces re-issuance, which can hit rate limits.
- Caddy's directive ordering is implicit and fixed (unlike nginx where order in config file matters) — use `route {}` blocks if you need explicit ordering.

## Related Units

- [nginx.conf Reference for Common Scenarios](/software/devops/nginx-common-configs/2026)
