Post

Caddy Comprehensive Guide - Installation, Configuration, and Security Hardening

Caddy is a powerful, enterprise-ready, open source web server written in Go. It's known for its simplicity, high performance, and automatic HTTPS. Unlike traditional web servers like Apache or Nginx, Caddy makes security the default with automatic TLS certificate provisioning and renewal through Let's Encrypt.

Caddy Comprehensive Guide - Installation, Configuration, and Security Hardening

Caddy Web Server Guide

Table of Contents

  1. Introduction
  2. Key Features
  3. Installation
  4. Basic Configuration
  5. Caddyfile Syntax
  6. HTTPS and Automatic TLS
  7. Reverse Proxy
  8. Static File Serving
  9. API Usage
  10. Performance Optimization
  11. Advanced Security Configuration
  12. Plugins and Extending Caddy
  13. Docker Integration
  14. Scaling Caddy
  15. Monitoring and Logging
  16. Troubleshooting
  17. Real-world Examples
  18. Migration from Other Web Servers
  19. Caddy vs. Alternatives
  20. Resources

Introduction

Originally created by Matthew Holt in 2015, Caddy has evolved into a production-ready server with a growing community and enterprise adoption. Caddy 2, released in 2020, represents a complete rewrite with an API-first design and modular architecture.

Key Features

  • Automatic HTTPS: Provisions and renews TLS certificates automatically
  • Simple Configuration: Human-readable Caddyfile format
  • API-driven: Everything can be controlled via a REST API
  • Extensible: Modular architecture with plugins
  • Fast Performance: Written in Go for high concurrency
  • Cross-platform: Runs on Windows, macOS, Linux, BSD, and more
  • Zero-downtime Reloads: Configuration changes without dropping connections
  • Modern HTTP: HTTP/2 and HTTP/3 support out of the box

Installation

Method 1: Official Binaries

1
2
3
4
5
6
7
# Linux x64
curl -OL https://github.com/caddyserver/caddy/releases/latest/download/caddy_2.7.5_linux_amd64.tar.gz
tar -xzf caddy_2.7.5_linux_amd64.tar.gz
sudo mv caddy /usr/local/bin/

# Verify installation
caddy version

Method 2: Package Managers

For Debian/Ubuntu:

1
2
3
4
5
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

For macOS:

1
brew install caddy

Method 3: Docker

1
2
docker pull caddy
docker run -p 80:80 -p 443:443 caddy

Method 4: Build from Source

Requires Go 1.18 or newer:

1
go install github.com/caddyserver/caddy/v2/cmd/caddy@latest

Basic Configuration

The Caddyfile

Caddy’s main configuration file is called the Caddyfile. It uses a simple, human-readable syntax.

Basic Caddyfile example:

1
2
3
4
example.com {
    root * /var/www/html
    file_server
}

This configuration:

  • Serves the domain example.com
  • Sets the root directory to /var/www/html
  • Enables the file server to serve static files

Starting Caddy

1
2
3
4
5
# Start Caddy with a Caddyfile
caddy run --config /path/to/Caddyfile

# Reload configuration
caddy reload --config /path/to/Caddyfile

Caddyfile Syntax

The Caddyfile has a simple structure:

1
2
3
<site> {
    <directive> [<arguments>]
}

Site Address

The site address can be:

  • A domain: example.com
  • With port: example.com:8080
  • Multiple domains: example.com, www.example.com
  • Path matching: example.com/api/*
  • Wildcard: *.example.com

Global Options

Global options are defined at the top of the Caddyfile:

1
2
3
4
5
6
7
8
{
    email [email protected]
    acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

example.com {
    # Site configuration
}

Common Directives

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Serve static files
file_server [browse]

# Set the root directory
root [<matcher>] <path>

# Handle errors
handle_errors {
    respond "<h1>Error {http.error.status_code}</h1>"
}

# Redirects
redir <from> <to> [<code>]

# Headers
header [<matcher>] [+|-]<field> [<value>]

HTTPS and Automatic TLS

Automatic HTTPS

By default, Caddy automatically obtains and manages TLS certificates for all sites:

1
2
3
4
5
example.com {
    # Caddy will automatically get certificates
    # and redirect HTTP to HTTPS
    file_server
}

Manual TLS Configuration

For custom certificate management:

1
2
3
example.com {
    tls /path/to/cert.pem /path/to/key.pem
}

Using Let’s Encrypt Staging Environment

For testing without hitting rate limits:

1
2
3
{
    acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

Custom Certificate Authority

1
2
3
4
5
example.com {
    tls {
        ca https://custom-ca.example.com/acme
    }
}

Reverse Proxy

Basic Reverse Proxy

1
2
3
4
example.com {
    # Forward all requests to a backend server
    reverse_proxy localhost:8080
}

Load Balancing

1
2
3
4
5
6
example.com {
    # Load balance between multiple backends
    reverse_proxy localhost:8081 localhost:8082 localhost:8083 {
        lb_policy round_robin
    }
}

This configuration:

  • Distributes requests among three backend servers
  • Uses round-robin load balancing algorithm

Health Checks

1
2
3
4
5
6
7
8
9
10
11
12
example.com {
    reverse_proxy localhost:8080 localhost:8081 {
        # Check health of backends
        health_path /health
        health_interval 10s
        health_timeout 5s
        health_status 200
        
        # Remove unhealthy hosts for 30 seconds
        fail_duration 30s
    }
}

This setup:

  • Performs health checks on the /health endpoint
  • Checks every 10 seconds with a 5-second timeout
  • Expects a 200 status code for healthy backends
  • Removes unhealthy hosts for 30 seconds

Header Modifications

1
2
3
4
5
6
7
8
9
example.com {
    reverse_proxy localhost:8080 {
        # Add headers to the upstream request
        header_up X-Real-IP {remote_host}
        
        # Modify response headers
        header_down +Access-Control-Allow-Origin "*"
    }
}

Static File Serving

Basic File Server

1
2
3
4
example.com {
    root * /var/www/html
    file_server
}

Directory Browsing

1
2
3
4
example.com {
    root * /var/www/html
    file_server browse
}

Custom File Handling

1
2
3
4
5
6
7
8
example.com {
    root * /var/www/html
    
    # Serve index.html for all paths
    try_files {path} /index.html
    
    file_server
}

This configuration implements a common pattern for single-page applications (SPAs):

  • Tries to serve the requested file
  • Falls back to index.html if the file doesn’t exist

Hiding Files

1
2
3
4
5
6
7
8
9
example.com {
    root * /var/www/html
    
    # Hide dot files
    @hidden path */.*
    respond @hidden 404
    
    file_server
}

This setup:

  • Creates a matcher for paths containing dot files
  • Returns a 404 response for those paths
  • Serves all other files normally

API Usage

Caddy 2 is designed to be controlled via its API.

Basic API Usage

1
2
3
4
5
# Get the current configuration
curl localhost:2019/config/ | jq

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

Programmatic Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
  "apps": {
    "http": {
      "servers": {
        "example": {
          "listen": [":80"],
          "routes": [
            {
              "handle": [{
                "handler": "static_response",
                "body": "Hello, World!"
              }]
            }
          ]
        }
      }
    }
  }
}

This JSON configuration:

  • Creates an HTTP server listening on port 80
  • Responds with “Hello, World!” to all requests

Converting Between Caddyfile and JSON

1
2
3
4
5
# Convert Caddyfile to JSON
caddy adapt --config /path/to/Caddyfile

# Validate configuration
caddy validate --config /path/to/Caddyfile

Performance Optimization

Caching

1
2
3
4
5
6
7
8
9
10
11
example.com {
    root * /var/www/html
    
    # Cache static assets
    @static {
        path *.css *.js *.jpg *.png *.svg
    }
    header @static Cache-Control "public, max-age=31536000"
    
    file_server
}

This configuration:

  • Matches static files like CSS, JS, and images
  • Sets a long-term caching header for these files (1 year)

Compression

1
2
3
4
example.com {
    encode gzip zstd
    file_server
}

This enables automatic compression using:

  • gzip for broader compatibility
  • zstd for better compression with supported clients

Connection Limits

1
2
3
4
5
6
7
8
example.com {
    # Limit to 1000 concurrent connections
    limits {
        connection_limit 1000
    }
    
    file_server
}

Advanced Security Configuration

Content Security Policy

1
2
3
4
5
example.com {
    header Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted-cdn.com;"
    
    file_server
}

This header:

  • Restricts content sources to the same origin by default
  • Allows scripts from the same origin and a specific trusted CDN

Rate Limiting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
example.com {
    # Rate limit requests
    rate_limit {
        zone login_attempts {
            key {http.request.remote.host}
            events 5
            window 10m
        }
    }
    
    # Apply to specific paths
    @login path /login
    rate_limit @login login_attempts
    
    file_server
}

This configuration:

  • Creates a rate limit zone that tracks by IP address
  • Allows 5 requests per 10-minute window
  • Applies the rate limit only to the login path

Basic Authentication

1
2
3
4
5
6
7
8
9
10
11
example.com {
    # Protect a specific path
    @protected path /admin/*
    
    basic_auth @protected {
        # bcrypt hashed password: admin:password
        admin $2a$14$QxCj1r1eAwQ0A9AYMQyOW.7nJ2jcYVPMagPqT6wQz7Zm67f/GgP12
    }
    
    file_server
}

This setup:

  • Protects all paths under /admin/
  • Requires basic authentication with the specified credentials

JWT Authentication

1
2
3
4
5
6
7
8
9
10
11
12
13
example.com {
    jwt {
        path /api/*
        allow roles user admin
        deny roles banned
        validate_exp true
        issuer my-auth-server
        auth_url /login
        secret_file /path/to/jwt/secret
    }
    
    file_server
}

This configuration:

  • Validates JWT tokens for API requests
  • Allows users with “user” or “admin” roles
  • Denies users with the “banned” role
  • Validates token expiration time
  • Specifies the token issuer
  • Redirects to login when authentication fails

Plugins and Extending Caddy

Installing Custom Builds with xcaddy

1
2
3
4
5
6
7
8
# Install xcaddy
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

# Build Caddy with plugins
xcaddy build \
  --with github.com/caddyserver/ntlm-transport \
  --with github.com/greenpau/caddy-security \
  --with github.com/caddyserver/nginx-adapter

Custom Plugins Example

Using the security module for OAuth:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
example.com {
    route /auth* {
        security {
            oauth2 identity provider github {
                realm github
                driver github
                client_id {env.GITHUB_CLIENT_ID}
                client_secret {env.GITHUB_CLIENT_SECRET}
                scopes openid email profile
            }
            
            authentication portal myportal {
                crypto default token lifetime 3600
                crypto key sign-verify {env.JWT_SECRET}
                enable identity provider github
                ui {
                    theme basic
                    logo url https://example.com/logo.png
                }
            }
        }
    }
    
    route {
        jwt {
            auth_url /auth
            trusted_tokens {
                static_secret {env.JWT_SECRET}
                issuer myportal
            }
        }
        
        file_server
    }
}

This complex configuration:

  • Sets up GitHub OAuth integration
  • Creates an authentication portal
  • Secures routes with JWT verification
  • Falls back to the auth URL for unauthenticated requests

Docker Integration

Basic Docker Setup

1
2
3
4
5
6
FROM caddy:2

COPY Caddyfile /etc/caddy/Caddyfile
COPY site /srv

EXPOSE 80 443

Docker Compose Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: '3'

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./site:/srv
      - caddy_data:/data
      - caddy_config:/config

  app:
    image: myapp
    restart: unless-stopped
    expose:
      - "8080"

volumes:
  caddy_data:
  caddy_config:

Caddy as a Reverse Proxy in Docker

1
2
3
4
5
6
7
8
{
    email [email protected]
}

example.com {
    # Use Docker DNS for service discovery
    reverse_proxy app:8080
}

Scaling Caddy

Horizontal Scaling

For horizontal scaling with multiple Caddy instances:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
version: '3'

services:
  caddy-lb:
    image: caddy:2
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile-lb:/etc/caddy/Caddyfile
    networks:
      - caddy-network

  caddy-1:
    image: caddy:2
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
    networks:
      - caddy-network

  caddy-2:
    image: caddy:2
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
    networks:
      - caddy-network

networks:
  caddy-network:

Caddyfile-lb for the load balancer:

1
2
3
4
5
6
example.com {
    reverse_proxy caddy-1:80 caddy-2:80 {
        lb_policy round_robin
        health_path /health
    }
}

Distributed Storage for Certificates

For distributed environments, you can configure custom storage:

1
2
3
4
5
6
7
8
9
10
{
    storage redis {
        address redis:6379
        password {env.REDIS_PASSWORD}
        db 0
        key_prefix caddy_
        timeout 5s
        tls_enabled false
    }
}

This configuration:

  • Uses Redis for certificate storage
  • Allows multiple Caddy instances to share certificates
  • Configures connection parameters for the Redis server

Monitoring and Logging

Structured Logging

1
2
3
4
5
6
{
    log {
        output file /var/log/caddy/access.log
        format json
    }
}

This configuration:

  • Outputs logs to a file in JSON format
  • Makes logs easily parsable by log analysis tools

Prometheus Metrics

1
2
3
4
5
6
example.com {
    # Expose metrics endpoint
    metrics /metrics
    
    file_server
}

This exposes Prometheus-compatible metrics at /metrics.

Custom Log Filtering

1
2
3
4
5
6
7
8
9
10
11
12
{
    log {
        output file /var/log/caddy/access.log
        format json
        
        # Exclude healthcheck paths
        exclude path /health /ping
        
        # Include only error responses
        include where {http.response.status} >= 400
    }
}

This advanced logging configuration:

  • Excludes specified paths from logs
  • Includes only responses with status codes >= 400
  • Uses JSON format for structured logging

Troubleshooting

Debug Mode

1
2
# Run Caddy with debug output
caddy run --config /path/to/Caddyfile --debug

Common Issues and Solutions

  1. Certificate issues:
    1
    2
    3
    4
    5
    
    example.com {
        tls {
            dns cloudflare {env.CLOUDFLARE_API_TOKEN}
        }
    }
    

    This uses DNS validation when HTTP validation fails.

  2. Permission problems:
    1
    2
    
    # Give Caddy permission to bind to privileged ports
    sudo setcap cap_net_bind_service=+ep /usr/bin/caddy
    
  3. Path issues:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    example.com {
        # Log the file path being accessed
        log {
            output stdout
            format console
            level DEBUG
        }
           
        # Add more verbose path debugging
        @debug path *
        handle @debug {
            respond "Requested path: {http.request.uri.path}"
        }
    }
    

Real-world Examples

WordPress Setup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
wordpress.example.com {
    root * /var/www/wordpress
    
    # PHP FastCGI handling
    php_fastcgi unix//run/php/php8.1-fpm.sock {
        index index.php
    }
    
    # Rewrite for WordPress pretty URLs
    rewrite {
        to {path} {path}/ /index.php?{query}
    }
    
    # Cache static assets
    @static {
        path *.ico *.css *.js *.gif *.jpg *.jpeg *.png *.svg *.woff *.woff2
    }
    header @static Cache-Control "public, max-age=604800"
    
    # Prevent access to sensitive files
    @forbidden {
        path /wp-config.php /.htaccess
    }
    respond @forbidden 403
    
    file_server
}

This comprehensive WordPress configuration:

  • Handles PHP processing through FastCGI
  • Sets up proper URL rewriting for WordPress
  • Caches static assets with appropriate headers
  • Blocks access to sensitive files

Microservices Architecture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
api.example.com {
    # API Gateway
    
    # Auth service
    handle_path /auth/* {
        reverse_proxy auth-service:8000
    }
    
    # User service
    handle_path /users/* {
        reverse_proxy user-service:8001
    }
    
    # Product service
    handle_path /products/* {
        reverse_proxy product-service:8002
    }
    
    # Order service
    handle_path /orders/* {
        reverse_proxy order-service:8003
    }
    
    # Shared JWT validation for all services
    handle {
        # Validate JWT except for auth paths
        @requireAuth {
            not path /auth/*
            not method OPTIONS
        }
        
        jwt @requireAuth {
            auth_url /auth/login
            allow roles user admin
        }
        
        # Add headers for CORS
        header {
            Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
            Access-Control-Allow-Headers "Content-Type, Authorization"
            Access-Control-Allow-Origin "*"
            Access-Control-Max-Age "3600"
            defer
        }
        
        # Handle preflight requests
        @options method OPTIONS
        respond @options 204
    }
}

This configuration:

  • Routes API requests to appropriate microservices
  • Implements JWT authentication
  • Properly handles CORS headers
  • Manages preflight OPTIONS requests

Migration from Other Web Servers

From Nginx

Nginx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com;
    
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    
    location / {
        root /var/www/html;
        index index.html;
    }
    
    location /api/ {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Equivalent Caddyfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
example.com {
    root * /var/www/html
    
    # Route /api requests to backend
    handle_path /api/* {
        reverse_proxy localhost:8080
    }
    
    # Serve static files for all other paths
    handle {
        file_server
    }
}

Benefits of migration:

  • Automatic HTTPS with certificate management
  • Simplified configuration syntax
  • Zero-downtime reloads

From Apache

Apache:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<VirtualHost *:80>
    ServerName example.com
    Redirect permanent / https://example.com/
</VirtualHost>

<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /var/www/html
    
    SSLEngine on
    SSLCertificateFile /path/to/cert.pem
    SSLCertificateKeyFile /path/to/key.pem
    
    <Directory /var/www/html>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
    
    RewriteEngine On
    RewriteRule ^/api/(.*) http://localhost:8080/$1 [P,L]
</VirtualHost>

Equivalent Caddyfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
example.com {
    root * /var/www/html
    
    # Similar to .htaccess files
    php_fastcgi localhost:9000
    
    # API proxying
    handle_path /api/* {
        reverse_proxy localhost:8080
    }
    
    file_server
}

Caddy vs. Alternatives

FeatureCaddyNginxApacheTraefik
Automatic HTTPS✅ Native❌ Manual❌ Manual✅ Native
Config Simplicity✅ High⚠️ Medium⚠️ Medium✅ High
Performance✅ High✅ High⚠️ Medium✅ High
Extensibility✅ Native plugins⚠️ Modules✅ Modules✅ Middleware
API-first design✅ Yes❌ No❌ No✅ Yes
Zero-downtime reloads✅ Yes✅ Yes⚠️ Limited✅ Yes
Docker integration✅ Good✅ Good⚠️ Limited✅ Excellent
Community size⚠️ Growing✅ Large✅ Large✅ Large

Resources


This guide provides a comprehensive overview of Caddy web server, from basic setup to advanced configurations. Caddy’s simplicity, security-first approach, and powerful features make it an excellent choice for modern web hosting needs.

This post is licensed under CC BY 4.0 by the author.