Skip to main content
Back to Category

Caddy Web Server Setup: Automatic HTTPS and Reverse Proxy

Caddy web server installation, Caddyfile configuration, automatic Let's Encrypt SSL, reverse proxy, static site hosting, PHP-FPM integration, and performance settings.

Read time: 14 min DevOps & Automation
caddyweb serverhttpssslreverse proxylet's encryptdevops

Table of Contents

Caddy Web Server Setup: Automatic HTTPS and Reverse Proxy

Caddy is a modern web server that stands out with automatic HTTPS certificate management. Thanks to its Let's Encrypt integration, it automatically obtains, renews, and configures SSL certificates.

What is Caddy?

Caddy's standout features:

  • Automatic HTTPS: Certificate management with Let's Encrypt is fully automatic
  • Simple configuration: Caddyfile syntax is easy to read
  • HTTP/2 and HTTP/3: Enabled by default
  • Reverse proxy: Built-in load balancing and health checks
  • Zero downtime reload: No interruption on configuration changes

Caddy requires root privileges to use ports 80 and 443. This is automatically managed when running as a systemd service.

Caddy Installation

Installation via apt (Ubuntu/Debian)

hljs bash
# Add Caddy GPG key and repository
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
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

# Install Caddy
sudo apt update
sudo apt install caddy -y

# Check service status
sudo systemctl status caddy

Binary Installation

hljs bash
# Download latest version
VERSION=$(curl -s https://api.github.com/repos/caddyserver/caddy/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -L "https://github.com/caddyserver/caddy/releases/download/${VERSION}/caddy_${VERSION#v}_linux_amd64.tar.gz" -o caddy.tar.gz

# Extract and install
tar -xzf caddy.tar.gz
sudo mv caddy /usr/local/bin/
sudo chmod +x /usr/local/bin/caddy

# Check version
caddy version

Custom Build with xcaddy

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

# Build with additional modules
xcaddy build \
  --with github.com/caddy-dns/cloudflare \
  --with github.com/greenpau/caddy-security

Caddyfile Syntax

Basic Structure

hljs caddyfile
# Single site
example.com {
    root * /var/www/html
    file_server
}

# Multiple sites
example.com {
    respond "Hello example.com!"
}

api.example.com {
    reverse_proxy localhost:3000
}

Global Options

hljs caddyfile
# Global settings at the top of Caddyfile
{
    # Admin API port
    admin localhost:2019

    # Email (for Let's Encrypt)
    email admin@example.com

    # ACME server (staging for testing)
    # acme_ca https://acme-staging-v02.api.letsencrypt.org/directory

    # HTTP port
    http_port 80

    # HTTPS port
    https_port 443

    # Log format
    log {
        output file /var/log/caddy/access.log
        format json
    }
}

Automatic HTTPS

Caddy automatically obtains a Let's Encrypt certificate for your domain:

hljs caddyfile
# Just write the domain name - HTTPS is automatic!
example.com {
    root * /var/www/html
    file_server
    encode gzip
}

# www redirect
www.example.com {
    redir https://example.com{uri} permanent
}

# Wildcard certificate (requires DNS challenge)
*.example.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }
    @api host api.example.com
    handle @api {
        reverse_proxy localhost:3000
    }
}

Caddy automatically renews certificates. No manual intervention required. Certificates are stored in ~/.local/share/caddy/ or /var/lib/caddy/.

Reverse Proxy Configuration

Basic Reverse Proxy

hljs caddyfile
app.example.com {
    reverse_proxy localhost:3000
}

# Multiple upstreams (load balancing)
api.example.com {
    reverse_proxy {
        to localhost:3001 localhost:3002 localhost:3003
        lb_policy round_robin
        health_uri /health
        health_interval 30s
        health_timeout 5s
    }
}

Advanced Reverse Proxy

hljs caddyfile
app.example.com {
    reverse_proxy localhost:3000 {
        # Forward headers
        header_up Host {upstream_hostport}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}

        # Timeouts
        transport http {
            dial_timeout 5s
            response_header_timeout 30s
        }

        # Retry
        lb_retries 3
        lb_try_duration 5s
    }

    # Security headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy strict-origin-when-cross-origin
        -Server
    }
}

Static File Serving

hljs caddyfile
static.example.com {
    root * /var/www/static
    file_server {
        # Directory listing
        browse
        # Hide hidden files
        hide .git .env
    }
    encode gzip zstd

    # Cache headers
    header /assets/* Cache-Control "public, max-age=31536000, immutable"
    header /*.html Cache-Control "no-cache"

    # Error pages
    handle_errors {
        rewrite * /errors/{err.status_code}.html
        file_server
    }
}

# SPA (Single Page Application)
app.example.com {
    root * /var/www/app
    encode gzip
    try_files {path} /index.html
    file_server
}

PHP-FPM Integration

hljs caddyfile
php.example.com {
    root * /var/www/php
    encode gzip

    # Route PHP files to PHP-FPM
    php_fastcgi unix//run/php/php8.2-fpm.sock

    # Or via TCP
    # php_fastcgi localhost:9000

    file_server

    # For WordPress
    @notStatic {
        not path /wp-content/* /wp-includes/*
        not file
    }
    rewrite @notStatic /index.php
}

# Full WordPress configuration
wordpress.example.com {
    root * /var/www/wordpress
    encode gzip

    php_fastcgi unix//run/php/php8.2-fpm.sock

    @disallowed path /xmlrpc.php /wp-admin/install.php
    respond @disallowed 403

    @static path /wp-content/* /wp-includes/*
    handle @static {
        file_server
    }

    handle {
        try_files {path} /index.php
        php_fastcgi unix//run/php/php8.2-fpm.sock
    }
}

Caddy with Docker

hljs yaml
# docker-compose.yml
services:
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"  # HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
      - ./html:/var/www/html:ro
    environment:
      - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}

  app:
    build: ./app
    expose:
      - "3000"

volumes:
  caddy_data:
  caddy_config:
hljs caddyfile
# Docker Caddyfile
example.com {
    reverse_proxy app:3000
}

Management via API

Caddy supports dynamic configuration via REST API:

hljs bash
# View current configuration
curl http://localhost:2019/config/

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

# Reload Caddy (zero downtime)
caddy reload --config /etc/caddy/Caddyfile

# Validate configuration
caddy validate --config /etc/caddy/Caddyfile

# Format Caddyfile
caddy fmt --overwrite /etc/caddy/Caddyfile

Performance Settings

hljs caddyfile
{
    servers {
        max_header_size 1MB
        timeouts {
            read_body 10s
            read_header 10s
            write 30s
            idle 2m
        }
    }
}

example.com {
    # Compression
    encode {
        gzip 6
        zstd
        minimum_length 1024
    }

    reverse_proxy localhost:3000
}

Conclusion

Caddy meets modern web server needs with automatic HTTPS management and simple configuration. It requires much less configuration compared to Nginx or Apache while offering all the features needed for production environments. It's an ideal choice especially for those who want to automate Let's Encrypt certificate management.