Privacy Tools Guide

Caddy is a web server written in Go that automatically provisions and renews TLS certificates from Let’s Encrypt. There is no certbot, no cron jobs, no manual renewal — Caddy handles it all. This makes it the fastest way to get a self-hosted service behind HTTPS on a VPS.

Prerequisites

Step 1: Install Caddy

# Install from the official Caddy 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

sudo apt update && sudo apt install caddy

# Verify
caddy version
# v2.8.x

Caddy installs as a systemd service and starts listening on port 80 immediately. It serves a placeholder page until you configure your Caddyfile.

Step 2: Basic Caddyfile

The Caddyfile lives at /etc/caddy/Caddyfile. Edit it:

sudo nano /etc/caddy/Caddyfile

Minimal configuration to serve a static site with automatic HTTPS:

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

That is the entire configuration. Caddy:

  1. Detects that example.com is a real domain
  2. Provisions a Let’s Encrypt certificate automatically
  3. Serves files from /var/www/html
  4. Redirects HTTP to HTTPS automatically

Apply the configuration:

sudo systemctl reload caddy
sudo systemctl status caddy

Check certificate:

curl -vI https://example.com 2>&1 | grep -A2 "SSL certificate\|subject:\|issuer:"

Step 3: Reverse Proxy to a Backend

The most common use case — proxying to an application running on localhost:

example.com {
    reverse_proxy localhost:8080
}

api.example.com {
    reverse_proxy localhost:3000
}

Caddy forwards all headers and handles TLS termination. The backend only needs to listen on a local port.

For multiple services on subdomains:

# Wildcard cert (requires DNS challenge — see below)
*.example.com {
    @wiki host wiki.example.com
    @gitea host gitea.example.com
    @grafana host grafana.example.com

    handle @wiki {
        reverse_proxy localhost:3001
    }
    handle @gitea {
        reverse_proxy localhost:3000
    }
    handle @grafana {
        reverse_proxy localhost:3002
    }
}

Step 4: Security Headers

Add security headers globally using the header directive:

(security_headers) {
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
        Permissions-Policy "geolocation=(), microphone=(), camera=()"
        Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
        -Server
        -X-Powered-By
    }
}

example.com {
    import security_headers
    reverse_proxy localhost:8080
}

The -Server and -X-Powered-By lines remove those response headers entirely to reduce fingerprinting.

Step 5: Rate Limiting

Caddy’s rate limit module is not built-in but available as a plugin. For basic protection, use the respond directive to block excessive requests, or deploy the caddy-ratelimit plugin:

# Build caddy with rate limit module
xcaddy build --with github.com/mholt/caddy-ratelimit

sudo mv caddy /usr/bin/caddy
sudo systemctl restart caddy

Then in your Caddyfile:

example.com {
    rate_limit {
        zone dynamic {
            key {remote_host}
            events 100
            window 1m
        }
    }
    reverse_proxy localhost:8080
}

Step 6: Basic Authentication

Protect an internal service with an username/password:

# Generate a bcrypt-hashed password
caddy hash-password --plaintext 'your-strong-password'
# $2a$14$...
private.example.com {
    basicauth {
        alice $2a$14$...hashedpassword...
    }
    reverse_proxy localhost:8081
}

Step 7: Logging

Enable structured JSON access logs for security analysis:

example.com {
    log {
        output file /var/log/caddy/access.log {
            roll_size 100mb
            roll_keep 5
        }
        format json
        level INFO
    }
    reverse_proxy localhost:8080
}

Create the log directory:

sudo mkdir -p /var/log/caddy
sudo chown caddy:caddy /var/log/caddy

Parse logs:

# Top 10 IPs by request count
cat /var/log/caddy/access.log | jq -r '.request.remote_ip' | sort | uniq -c | sort -rn | head

# Show 404s
cat /var/log/caddy/access.log | jq 'select(.status == 404) | {uri: .request.uri, ip: .request.remote_ip}'

Step 8: DNS Challenge for Wildcard Certificates

Wildcard certs (*.example.com) require DNS-01 challenge, which means Caddy needs API access to your DNS provider. Install the appropriate DNS module:

# For Cloudflare
xcaddy build --with github.com/caddy-dns/cloudflare

# In Caddyfile, provide the API token
*.example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    # ... your handlers
}

Set the environment variable:

# /etc/systemd/system/caddy.service.d/override.conf
[Service]
Environment="CF_API_TOKEN=your_cloudflare_api_token"
sudo systemctl daemon-reload
sudo systemctl restart caddy

Verify Everything

# Check certificate status
sudo caddy certificates

# Validate your Caddyfile without restarting
caddy validate --config /etc/caddy/Caddyfile

# Test TLS configuration
curl -vI https://example.com

# Grade your TLS at Qualys SSL Labs
# https://www.ssllabs.com/ssltest/analyze.html?d=example.com