Networking beginner ⏱ 30 minutes

Caddy: The Automatic HTTPS Web Server

Zero-config HTTPS with automatic certificate management. Modern, secure web serving for your homelab.

By homelabbing.io • 2025-12-16
#caddy#reverse-proxy#https#web-server#automatic-ssl

Introduction

Caddy is a powerful, enterprise-ready web server with automatic HTTPS that’s perfect for homelabs. Unlike traditional web servers that require complex configuration files and manual certificate management, Caddy handles HTTPS automatically through Let’s Encrypt integration, making secure deployments effortless.

In this comprehensive guide, you’ll learn how to deploy Caddy as a reverse proxy, configure automatic HTTPS, integrate with Docker, and leverage AI automation to streamline your homelab infrastructure.

What You’ll Learn

  • Understanding Caddy’s architecture and core features
  • Installing and configuring Caddy on Linux
  • Setting up automatic HTTPS with Let’s Encrypt
  • Configuring reverse proxy for homelab services
  • Docker and Docker Compose deployment
  • Comparing Caddy with Nginx, Traefik, and Nginx Proxy Manager
  • Advanced features: load balancing, middleware, and templates
  • AI-powered configuration generation and monitoring
  • Security best practices and troubleshooting

Why Caddy?

Automatic HTTPS by Default: Caddy automatically obtains and renews SSL certificates from Let’s Encrypt without any configuration. Just specify your domain, and Caddy handles the rest.

Simple Configuration: Caddyfile syntax is human-readable and intuitive. No cryptic directives or complex regex patterns - just straightforward, declarative configuration.

Modern by Design: Built in Go, Caddy is a single binary with no dependencies. It supports HTTP/2, HTTP/3 (QUIC), and modern web standards out of the box.

Perfect for Homelabs: Caddy’s simplicity and automatic HTTPS make it ideal for homelab environments where you want security without complexity.

Prerequisites

Hardware Requirements:

  • CPU: 1 core minimum (2+ recommended)
  • RAM: 512MB minimum (1GB+ recommended)
  • Storage: 100MB for binary, 1GB+ for logs and certificates
  • Network: Port 80 and 443 access required for HTTP-01 challenge

Knowledge Requirements:

  • Basic Linux command line skills
  • Understanding of DNS and domain management
  • Familiarity with reverse proxy concepts
  • Basic Docker knowledge (for containerized deployment)

Infrastructure:

  • Domain name or dynamic DNS service
  • DNS A record pointing to your server
  • Firewall allowing ports 80 and 443
  • Optional: Docker for containerized deployment

Time Commitment:

  • Initial setup: 20-30 minutes
  • Configuration: 30-60 minutes
  • Advanced features: 1-2 hours

Understanding Caddy’s Core Concepts

The Caddyfile

The Caddyfile is Caddy’s primary configuration format. It’s designed to be simple and human-readable:

example.com {
    reverse_proxy localhost:8080
}

That’s it. This configuration:

  1. Serves traffic for example.com
  2. Automatically obtains SSL certificate
  3. Redirects HTTP to HTTPS
  4. Proxies requests to localhost:8080
  5. Automatically renews the certificate

Automatic HTTPS

Caddy’s killer feature is zero-configuration HTTPS:

How it works:

  1. Caddy detects domain names in your configuration
  2. Contacts Let’s Encrypt to obtain certificates
  3. Completes ACME challenge (HTTP-01 or TLS-ALPN-01)
  4. Serves traffic with HTTPS enabled
  5. Automatically renews certificates before expiration

Requirements:

  • Public domain name
  • DNS A/AAAA record pointing to your server
  • Ports 80 and 443 accessible from internet
  • Valid email address for Let’s Encrypt account

Directives and Matchers

Directives are instructions in the Caddyfile:

example.com {
    # Directives
    root * /var/www
    file_server
    encode gzip
    log {
        output file /var/log/caddy/access.log
    }
}

Matchers filter which requests a directive applies to:

example.com {
    # Match only /api paths
    @api {
        path /api/*
    }
    reverse_proxy @api localhost:3000

    # Match everything else
    root * /var/www
    file_server
}

JSON Configuration

For advanced use cases, Caddy uses JSON as its native configuration:

{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "listen": [":443"],
          "routes": [{
            "match": [{
              "host": ["example.com"]
            }],
            "handle": [{
              "handler": "reverse_proxy",
              "upstreams": [{"dial": "localhost:8080"}]
            }]
          }]
        }
      }
    }
  }
}

The Caddyfile is actually converted to JSON under the hood. For most homelab use cases, stick with the Caddyfile format.


Installation Methods

Download and install the official Caddy binary:

Ubuntu/Debian:

# Install dependencies
sudo apt update
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https

# Add Caddy repository
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

# Verify installation
caddy version

RHEL/CentOS/Fedora:

# Install dependencies
sudo dnf install 'dnf-command(copr)'

# Enable Caddy repository
sudo dnf copr enable @caddy/caddy

# Install Caddy
sudo dnf install caddy

# Verify installation
caddy version

Manual Binary Installation:

# Download latest Caddy binary
curl -o caddy.tar.gz 'https://caddyserver.com/api/download?os=linux&arch=amd64'

# Extract
tar -xzf caddy.tar.gz

# Move to system path
sudo mv caddy /usr/local/bin/

# Verify
caddy version

# Create Caddy user
sudo useradd -r -d /var/lib/caddy -s /usr/sbin/nologin caddy

# Create directories
sudo mkdir -p /etc/caddy /var/lib/caddy /var/log/caddy
sudo chown -R caddy:caddy /etc/caddy /var/lib/caddy /var/log/caddy

Method 2: Docker Deployment

Run Caddy in a Docker container:

Basic Docker Run:

docker run -d   --name caddy   -p 80:80   -p 443:443   -v $PWD/Caddyfile:/etc/caddy/Caddyfile   -v caddy_data:/data   -v caddy_config:/config   caddy:latest

Docker Compose (Recommended):

Create docker-compose.yml:

version: '3.8'

services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"  # HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./site:/srv
      - caddy_data:/data
      - caddy_config:/config
    environment:
      - [email protected]
    networks:
      - caddy_network

volumes:
  caddy_data:
  caddy_config:

networks:
  caddy_network:
    driver: bridge

Deploy:

# Create Caddyfile (see next section)
touch Caddyfile

# Start Caddy
docker-compose up -d

# View logs
docker-compose logs -f caddy

# Reload configuration
docker-compose exec caddy caddy reload --config /etc/caddy/Caddyfile

Method 3: Build from Source

For custom modules or latest development version:

# Install Go (1.21+)
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

# Install xcaddy (Caddy builder)
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

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

# Move to system path
sudo mv caddy /usr/local/bin/

# Verify
caddy version

Initial Configuration

Create your first Caddyfile:

sudo nano /etc/caddy/Caddyfile

Add basic configuration:

# Global options
{
    email [email protected]
    admin off  # Disable admin API for security
}

# Simple static site
example.com {
    root * /var/www/html
    file_server
    encode gzip
    log {
        output file /var/log/caddy/access.log {
            roll_size 100mb
            roll_keep 5
        }
    }
}

Test configuration:

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

# Test run
sudo caddy run --config /etc/caddy/Caddyfile

# If working, enable systemd service
sudo systemctl enable caddy
sudo systemctl start caddy
sudo systemctl status caddy

Reverse Proxy Configuration

Caddy excels as a reverse proxy for homelab services.

Basic Reverse Proxy

Proxy a single service:

homeassistant.example.com {
    reverse_proxy localhost:8123
}

That’s it! Caddy will:

  • Automatically get SSL certificate
  • Proxy all requests to Home Assistant
  • Handle WebSocket connections
  • Add security headers

Multiple Services

Proxy multiple homelab services:

# Home Assistant
homeassistant.example.com {
    reverse_proxy localhost:8123
}

# Jellyfin Media Server
jellyfin.example.com {
    reverse_proxy localhost:8096
}

# Portainer
portainer.example.com {
    reverse_proxy localhost:9000
}

# Nextcloud
nextcloud.example.com {
    reverse_proxy localhost:8080

    # Nextcloud-specific headers
    header {
        Strict-Transport-Security "max-age=31536000;"
    }

    # Increase timeouts for large file uploads
    reverse_proxy {
        header_up X-Forwarded-Ssl on
    }
}

Path-Based Routing

Route different paths to different backends:

example.com {
    # API requests go to backend
    handle /api/* {
        reverse_proxy localhost:3000
    }

    # Admin panel
    handle /admin/* {
        reverse_proxy localhost:9000
    }

    # Everything else serves static files
    handle {
        root * /var/www/html
        file_server
    }
}

Docker Service Discovery

Proxy to Docker containers:

# Using container name on Docker network
jellyfin.example.com {
    reverse_proxy jellyfin:8096
}

# Multiple container instances (load balancing)
api.example.com {
    reverse_proxy {
        to api-1:3000
        to api-2:3000
        to api-3:3000

        lb_policy round_robin
        health_uri /health
        health_interval 30s
    }
}

Docker Compose integration:

version: '3.8'

services:
  caddy:
    image: caddy:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
    networks:
      - homelab

  jellyfin:
    image: jellyfin/jellyfin
    container_name: jellyfin
    networks:
      - homelab

  homeassistant:
    image: homeassistant/home-assistant
    container_name: homeassistant
    networks:
      - homelab

networks:
  homelab:
    driver: bridge

volumes:
  caddy_data:

WebSocket Support

Caddy handles WebSocket connections automatically:

# No special configuration needed!
websocket-app.example.com {
    reverse_proxy localhost:8080
}

# Explicit WebSocket handling (optional)
ws.example.com {
    reverse_proxy localhost:8080 {
        header_up Upgrade {>Upgrade}
        header_up Connection {>Connection}
    }
}

Custom Headers

Add security and custom headers:

secure.example.com {
    reverse_proxy localhost:8080

    header {
        # Security headers
        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"

        # Remove server information
        -Server

        # Custom headers
        X-Powered-By "My Homelab"
    }
}

Automatic HTTPS Configuration

Let’s Encrypt Integration

Caddy automatically uses Let’s Encrypt for SSL certificates:

# Automatic HTTPS (default)
example.com {
    reverse_proxy localhost:8080
}

# Specify email for Let's Encrypt
{
    email [email protected]
}

example.com {
    reverse_proxy localhost:8080
}

DNS Challenge (Wildcard Certificates)

For wildcard certificates or when ports 80/443 aren’t accessible:

Install Caddy with DNS plugin:

xcaddy build --with github.com/caddy-dns/cloudflare

Configure DNS challenge:

{
    email [email protected]
}

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

    @homeassistant host homeassistant.example.com
    handle @homeassistant {
        reverse_proxy localhost:8123
    }

    @jellyfin host jellyfin.example.com
    handle @jellyfin {
        reverse_proxy localhost:8096
    }
}

Environment variables:

export CLOUDFLARE_API_TOKEN="your_token_here"

Docker Compose with DNS challenge:

services:
  caddy:
    image: caddy:latest
    environment:
      - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
    command: caddy run --config /etc/caddy/Caddyfile

Custom Certificate Authority

Use your own CA or staging environment:

{
    # Use Let's Encrypt staging for testing
    acme_ca https://acme-staging-v02.api.letsencrypt.org/directory

    # Or use ZeroSSL
    # acme_ca https://acme.zerossl.com/v2/DV90
}

example.com {
    reverse_proxy localhost:8080
}

Local HTTPS (Self-Signed)

For local development or internal services:

{
    # Use internal CA
    local_certs
}

homelab.local {
    reverse_proxy localhost:8080
}

Caddy will generate self-signed certificates. Trust the Caddy CA:

# Find Caddy's root CA
sudo caddy trust

# Or manually
sudo cp ~/.local/share/caddy/pki/authorities/local/root.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

Certificate Management

Monitor and manage certificates:

# List certificates
caddy list-certificates

# Force certificate renewal
caddy reload --config /etc/caddy/Caddyfile

# Certificate storage location
ls -la ~/.local/share/caddy/certificates/

# Docker volume location
docker volume inspect caddy_data

Advanced Configuration

Load Balancing

Distribute traffic across multiple backends:

api.example.com {
    reverse_proxy {
        # Backend servers
        to backend1.local:8080
        to backend2.local:8080
        to backend3.local:8080

        # Load balancing policy
        lb_policy round_robin  # or: least_conn, ip_hash, first, random

        # Health checks
        health_uri /health
        health_port 8080
        health_interval 30s
        health_timeout 5s
        health_status 2xx

        # Failover
        fail_duration 30s
        max_fails 3
        unhealthy_status 5xx

        # Connection pooling
        max_conns_per_host 100

        # Retry on failure
        lb_try_duration 5s
        lb_try_interval 1s
    }
}

Rate Limiting

Protect services from abuse:

# Install rate limit module
xcaddy build --with github.com/mholt/caddy-ratelimit
api.example.com {
    # Rate limit: 100 requests per minute per IP
    rate_limit {
        zone api {
            key {remote_host}
            events 100
            window 1m
        }
    }

    reverse_proxy localhost:3000
}

Authentication

Add basic authentication:

admin.example.com {
    # Generate password: caddy hash-password --plaintext mypassword
    basicauth {
        admin $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
        user2 $2a$14$another_hash_here
    }

    reverse_proxy localhost:9000
}

Generate password hash:

caddy hash-password --plaintext yourpassword

File Server with Browse

Serve files with directory browsing:

files.example.com {
    root * /mnt/storage/public
    file_server browse {
        hide .git
        hide .env
    }

    encode gzip

    # Custom browse template
    templates
}

Reverse Proxy with Caching

Cache responses for better performance:

# Install cache module
xcaddy build --with github.com/caddyserver/cache-handler
cached.example.com {
    reverse_proxy localhost:8080 {
        header_up Host {upstream_hostport}
    }

    cache {
        ttl 1h
        stale 1h
    }
}

Templates and Dynamic Content

Use templates for dynamic responses:

dynamic.example.com {
    root * /var/www/dynamic
    templates
    file_server
}

Create index.html:

<!DOCTYPE html>
<html>
<head>
    <title>Dynamic Page</title>
</head>
<body>
    <h1>Server Information</h1>
    <p>Request from: {{.RemoteIP}}</p>
    <p>Host: {{.Host}}</p>
    <p>Current time: {{now | date "2006-01-02 15:04:05"}}</p>
    <p>User Agent: {{.Req.Header.Get "User-Agent"}}</p>

    {{if .Req.URL.Query.Get "name"}}
        <p>Hello, {{.Req.URL.Query.Get "name"}}!</p>
    {{end}}
</body>
</html>

Logging

Configure advanced logging:

{
    log {
        output file /var/log/caddy/caddy.log {
            roll_size 100mb
            roll_keep 10
            roll_keep_days 90
        }
        format json
        level INFO
    }
}

example.com {
    # Per-site logging
    log {
        output file /var/log/caddy/example.com.log {
            roll_size 50mb
            roll_keep 5
        }
        format transform "{common_log}"
    }

    reverse_proxy localhost:8080
}

Snippets for Reusable Configuration

Define reusable configuration blocks:

(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"
        -Server
    }
}

(proxy_settings) {
    header_up Host {upstream_hostport}
    header_up X-Forwarded-For {remote_host}
    header_up X-Forwarded-Proto {scheme}
}

# Use snippets
app1.example.com {
    import security_headers
    reverse_proxy localhost:8080 {
        import proxy_settings
    }
}

app2.example.com {
    import security_headers
    reverse_proxy localhost:9000 {
        import proxy_settings
    }
}

Caddy vs. Other Reverse Proxies

Caddy vs. Nginx

FeatureCaddyNginx
ConfigurationSimple CaddyfileComplex nginx.conf
Automatic HTTPSYes (built-in)No (requires certbot)
HTTP/2AutomaticManual config
HTTP/3Built-inRequires recompile
Certificate RenewalAutomaticManual/cron job
Learning CurveEasyModerate to difficult
PerformanceExcellentExcellent
Memory Usage~20-50MB~5-20MB
EcosystemGrowingMature

When to use Caddy:

  • You want automatic HTTPS without complexity
  • Configuration simplicity is important
  • You’re new to reverse proxies
  • You need quick deployment

When to use Nginx:

  • You need absolute maximum performance
  • You have existing Nginx expertise
  • You need specific Nginx modules
  • Memory is extremely limited

Caddy vs. Traefik

FeatureCaddyTraefik
ConfigurationCaddyfile or JSONLabels or YAML
Automatic HTTPSYesYes
Docker IntegrationGoodExcellent
Kubernetes SupportPluginNative
DashboardOptionalBuilt-in
Learning CurveEasyModerate
Dynamic ConfigReload neededAutomatic
Resource UsageLowerHigher

When to use Caddy:

  • Simple static configuration
  • You prefer file-based config
  • Lower resource usage matters
  • You don’t need dynamic service discovery

When to use Traefik:

  • Heavy Docker/Kubernetes use
  • Need automatic service discovery
  • Want built-in dashboard
  • Managing many dynamic services

Caddy vs. Nginx Proxy Manager

FeatureCaddyNPM
InterfaceCLI/Config filesWeb GUI
ConfigurationText-basedClick-based
Automatic HTTPSYesYes
FlexibilityVery highLimited by GUI
Learning CurveEasyVery easy
Version ControlEasy (text files)Difficult
AutomationExcellentLimited
Resource UsageLowerHigher (GUI overhead)

When to use Caddy:

  • You prefer infrastructure-as-code
  • Need advanced configuration
  • Want version control
  • Comfortable with CLI

When to use NPM:

  • You prefer GUI management
  • Want visual configuration
  • Managing few services
  • Non-technical users

Docker Integration

Complete Docker Stack

Full homelab stack with Caddy:

version: '3.8'

services:
  # Caddy Reverse Proxy
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
      - caddy_logs:/var/log/caddy
    environment:
      - [email protected]
      - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
    networks:
      - homelab
    healthcheck:
      test: ["CMD", "wget", "--spider", "http://localhost:2019/config/"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Home Assistant
  homeassistant:
    image: homeassistant/home-assistant:latest
    container_name: homeassistant
    restart: unless-stopped
    environment:
      - TZ=America/New_York
    volumes:
      - ./homeassistant:/config
    networks:
      - homelab

  # Jellyfin Media Server
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    restart: unless-stopped
    environment:
      - TZ=America/New_York
    volumes:
      - ./jellyfin/config:/config
      - ./jellyfin/cache:/cache
      - /mnt/media:/media:ro
    networks:
      - homelab

  # Nextcloud
  nextcloud:
    image: nextcloud:latest
    container_name: nextcloud
    restart: unless-stopped
    environment:
      - MYSQL_HOST=nextcloud-db
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=${NEXTCLOUD_DB_PASSWORD}
    volumes:
      - ./nextcloud:/var/www/html
    networks:
      - homelab
    depends_on:
      - nextcloud-db

  nextcloud-db:
    image: mariadb:latest
    container_name: nextcloud-db
    restart: unless-stopped
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=${NEXTCLOUD_DB_PASSWORD}
    volumes:
      - ./nextcloud-db:/var/lib/mysql
    networks:
      - homelab

  # Portainer
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    restart: unless-stopped
    command: -H unix:///var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./portainer:/data
    networks:
      - homelab

volumes:
  caddy_data:
  caddy_config:
  caddy_logs:

networks:
  homelab:
    driver: bridge

Corresponding Caddyfile:

{
    email {$ACME_EMAIL}
}

# Home Assistant
homeassistant.example.com {
    reverse_proxy homeassistant:8123

    # WebSocket support
    header Connection "Upgrade"
    header Upgrade "websocket"
}

# Jellyfin
jellyfin.example.com {
    reverse_proxy jellyfin:8096

    header {
        X-Frame-Options "SAMEORIGIN"
    }
}

# Nextcloud
nextcloud.example.com {
    reverse_proxy nextcloud:80

    header {
        Strict-Transport-Security "max-age=31536000;"
    }

    # Nextcloud CalDAV/CardDAV
    redir /.well-known/carddav /remote.php/dav 301
    redir /.well-known/caldav /remote.php/dav 301

    # Large file uploads
    request_body {
        max_size 10GB
    }
}

# Portainer
portainer.example.com {
    reverse_proxy portainer:9000
}

Hot Reload Configuration

Reload Caddy without downtime:

# Using Docker
docker exec caddy caddy reload --config /etc/caddy/Caddyfile

# Or with compose
docker-compose exec caddy caddy reload --config /etc/caddy/Caddyfile

# View current config
docker exec caddy caddy adapt --config /etc/caddy/Caddyfile --pretty

AI Automation and Smart Configuration

AI-Generated Caddyfile

Use AI to generate configurations from natural language:

#!/usr/bin/env python3
"""
AI-powered Caddy configuration generator
Converts natural language service descriptions to Caddyfile entries
"""

import re
import json
from typing import Dict, List

class CaddyConfigGenerator:
    def __init__(self):
        self.services = {}
        self.security_presets = {
            'high': {
                'headers': [
                    'Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"',
                    'X-Content-Type-Options "nosniff"',
                    'X-Frame-Options "DENY"',
                    'X-XSS-Protection "1; mode=block"',
                    'Referrer-Policy "strict-origin-when-cross-origin"'
                ],
                'tls': 'tls_min_version 1.3'
            },
            'medium': {
                'headers': [
                    'Strict-Transport-Security "max-age=31536000;"',
                    'X-Content-Type-Options "nosniff"',
                    'X-Frame-Options "SAMEORIGIN"'
                ]
            },
            'low': {
                'headers': []
            }
        }

    def parse_service_description(self, description: str) -> Dict:
        """Parse natural language service description"""
        service = {
            'domain': None,
            'backend': None,
            'port': None,
            'security': 'medium',
            'features': []
        }

        # Extract domain
        domain_match = re.search(r'(?:domain|host|site)[s:]+(S+.com|S+.local)', description, re.IGNORECASE)
        if domain_match:
            service['domain'] = domain_match.group(1)

        # Extract port
        port_match = re.search(r'(?:port|:)[s]*(d+)', description)
        if port_match:
            service['port'] = port_match.group(1)

        # Detect service type
        if any(word in description.lower() for word in ['homeassistant', 'home assistant', 'hass']):
            service['backend'] = 'homeassistant'
            service['port'] = service['port'] or '8123'
            service['features'].append('websocket')

        elif any(word in description.lower() for word in ['jellyfin', 'media', 'plex']):
            service['backend'] = 'jellyfin'
            service['port'] = service['port'] or '8096'

        elif any(word in description.lower() for word in ['nextcloud', 'cloud']):
            service['backend'] = 'nextcloud'
            service['port'] = service['port'] or '80'
            service['features'].append('nextcloud-headers')
            service['features'].append('large-uploads')

        elif any(word in description.lower() for word in ['portainer', 'docker', 'container']):
            service['backend'] = 'portainer'
            service['port'] = service['port'] or '9000'

        # Detect security level
        if any(word in description.lower() for word in ['secure', 'high security', 'locked down']):
            service['security'] = 'high'
        elif any(word in description.lower() for word in ['basic', 'low security']):
            service['security'] = 'low'

        return service

    def generate_config(self, service: Dict) -> str:
        """Generate Caddyfile configuration"""
        if not service['domain'] or not service['backend']:
            return "# ERROR: Missing domain or backend"

        config_lines = [f"{service['domain']} {{"]

        # Reverse proxy
        if service['backend'].startswith('http'):
            config_lines.append(f"    reverse_proxy {service['backend']}")
        else:
            config_lines.append(f"    reverse_proxy {service['backend']}:{service['port']}")

        # Security headers
        security_preset = self.security_presets.get(service['security'], self.security_presets['medium'])
        if security_preset['headers']:
            config_lines.append("    header {")
            for header in security_preset['headers']:
                config_lines.append(f"        {header}")
            config_lines.append("    }")

        # TLS configuration
        if 'tls_min_version' in security_preset:
            config_lines.append(f"    {security_preset['tls']}")

        # Feature-specific configuration
        if 'websocket' in service['features']:
            config_lines.append('    header Connection "Upgrade"')
            config_lines.append('    header Upgrade "websocket"')

        if 'nextcloud-headers' in service['features']:
            config_lines.append('    redir /.well-known/carddav /remote.php/dav 301')
            config_lines.append('    redir /.well-known/caldav /remote.php/dav 301')

        if 'large-uploads' in service['features']:
            config_lines.append('    request_body {')
            config_lines.append('        max_size 10GB')
            config_lines.append('    }')

        config_lines.append("}")

        return "\n".join(config_lines)

    def generate_from_description(self, description: str) -> str:
        """Generate Caddyfile from natural language description"""
        service = self.parse_service_description(description)
        return self.generate_config(service)

# Usage
if __name__ == "__main__":
    generator = CaddyConfigGenerator()

    # Example 1: Home Assistant
    config1 = generator.generate_from_description(
        "I want to proxy Home Assistant on homeassistant.example.com port 8123 with high security"
    )
    print("Home Assistant Configuration:")
    print(config1)
    print()

    # Example 2: Jellyfin
    config2 = generator.generate_from_description(
        "Setup Jellyfin media server at jellyfin.example.com on port 8096"
    )
    print("Jellyfin Configuration:")
    print(config2)
    print()

    # Example 3: Nextcloud
    config3 = generator.generate_from_description(
        "Configure Nextcloud at cloud.example.com with secure headers"
    )
    print("Nextcloud Configuration:")
    print(config3)

Run the generator:

python3 caddy_config_generator.py

Automated Health Monitoring

Monitor Caddy and backend services:

#!/usr/bin/env python3
"""
Caddy Health Monitor
Monitors Caddy admin API and backend service health
"""

import requests
import json
import time
from datetime import datetime
from typing import Dict, List

class CaddyHealthMonitor:
    def __init__(self, admin_endpoint='http://localhost:2019'):
        self.admin_endpoint = admin_endpoint
        self.health_history = []

    def get_config(self) -> Dict:
        """Get current Caddy configuration"""
        try:
            response = requests.get(f"{self.admin_endpoint}/config/")
            return response.json()
        except Exception as e:
            return {'error': str(e)}

    def get_server_status(self) -> Dict:
        """Get server status from admin API"""
        try:
            response = requests.get(f"{self.admin_endpoint}/reverse_proxy/upstreams")
            return response.json()
        except Exception as e:
            return {'error': str(e)}

    def check_backend_health(self, url: str) -> bool:
        """Check if backend is responding"""
        try:
            response = requests.get(url, timeout=5, verify=False)
            return 200 <= response.status_code < 500
        except:
            return False

    def analyze_upstreams(self) -> List[Dict]:
        """Analyze upstream health"""
        upstreams = self.get_server_status()
        if 'error' in upstreams:
            return []

        results = []
        for upstream_group, details in upstreams.items():
            for upstream in details:
                status = {
                    'name': upstream.get('address', 'unknown'),
                    'healthy': upstream.get('healthy', False),
                    'num_requests': upstream.get('num_requests', 0),
                    'fails': upstream.get('fails', 0)
                }
                results.append(status)

        return results

    def generate_report(self) -> str:
        """Generate health report"""
        upstreams = self.analyze_upstreams()

        report = f"""
Caddy Health Report
Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
{'='*60}

Admin API Status: {'✓ Online' if self.get_config() else '✗ Offline'}

Upstream Services:
"""

        for upstream in upstreams:
            status_icon = '✓' if upstream['healthy'] else '✗'
            report += f"""
  {status_icon} {upstream['name']}
     Healthy: {upstream['healthy']}
     Requests: {upstream['num_requests']}
     Failures: {upstream['fails']}
"""

        # Recommendations
        report += "\nRecommendations:\n"

        unhealthy_count = sum(1 for u in upstreams if not u['healthy'])
        if unhealthy_count > 0:
            report += f"  - CRITICAL: {unhealthy_count} upstream(s) unhealthy - investigate immediately\n"

        high_fail_upstreams = [u for u in upstreams if u['fails'] > 10]
        if high_fail_upstreams:
            report += f"  - WARNING: {len(high_fail_upstreams)} upstream(s) with high failure rate\n"

        if not upstreams:
            report += "  - INFO: No upstreams configured or monitoring unavailable\n"

        return report

    def monitor_continuously(self, interval: int = 60):
        """Continuously monitor and alert"""
        print(f"Starting Caddy health monitoring (interval: {interval}s)")

        while True:
            try:
                report = self.generate_report()

                # Check for critical issues
                upstreams = self.analyze_upstreams()
                unhealthy = [u for u in upstreams if not u['healthy']]

                if unhealthy:
                    print("\n" + "="*60)
                    print("ALERT: Unhealthy Upstreams Detected!")
                    print("="*60)
                    for upstream in unhealthy:
                        print(f"  - {upstream['name']}: {upstream['fails']} failures")
                    print()

                # Print periodic report every 10 minutes
                if int(time.time()) % 600 < interval:
                    print(report)

                time.sleep(interval)

            except KeyboardInterrupt:
                print("\nMonitoring stopped by user")
                break
            except Exception as e:
                print(f"Error during monitoring: {e}")
                time.sleep(interval)

# Usage
if __name__ == "__main__":
    import sys

    monitor = CaddyHealthMonitor()

    if len(sys.argv) > 1 and sys.argv[1] == 'report':
        # Single report
        print(monitor.generate_report())
    else:
        # Continuous monitoring
        monitor.monitor_continuously(interval=60)

Run as systemd service:

[Unit]
Description=Caddy Health Monitor
After=caddy.service
Requires=caddy.service

[Service]
Type=simple
User=caddy
ExecStart=/usr/bin/python3 /opt/caddy/health_monitor.py
Restart=on-failure
RestartSec=30

[Install]
WantedBy=multi-user.target

Security Best Practices

Enable Admin API with Authentication

Secure the admin API:

{
    admin :2019 {
        origins localhost 127.0.0.1
    }
}

Or disable if not needed:

{
    admin off
}

Implement Rate Limiting

Protect against DDoS and brute force:

# Build Caddy with rate limit module
xcaddy build --with github.com/mholt/caddy-ratelimit
api.example.com {
    rate_limit {
        zone api {
            key {remote_host}
            events 100
            window 1m
        }
    }

    reverse_proxy localhost:3000
}

Restrict Access by IP

Allow only specific IP ranges:

admin.example.com {
    @blocked {
        not remote_ip 192.168.1.0/24 10.0.0.0/8
    }

    handle @blocked {
        abort
    }

    reverse_proxy localhost:9000
}

Set Security Headers

Comprehensive security headers:

(security_headers) {
    header {
        # HSTS
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

        # Content Security Policy
        Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"

        # XSS Protection
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        X-XSS-Protection "1; mode=block"

        # Referrer Policy
        Referrer-Policy "strict-origin-when-cross-origin"

        # Permissions Policy
        Permissions-Policy "geolocation=(), microphone=(), camera=()"

        # Remove server info
        -Server
        -X-Powered-By
    }
}

secure-app.example.com {
    import security_headers
    reverse_proxy localhost:8080
}

Regular Backups

Backup Caddy configuration and certificates:

#!/bin/bash
# backup-caddy.sh

BACKUP_DIR="/backup/caddy"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# Create backup directory
mkdir -p "$BACKUP_DIR"

# Backup Caddyfile
cp /etc/caddy/Caddyfile "$BACKUP_DIR/Caddyfile.$TIMESTAMP"

# Backup certificates and data
tar -czf "$BACKUP_DIR/caddy-data.$TIMESTAMP.tar.gz"   ~/.local/share/caddy/

# Keep only last 30 days
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +30 -delete
find "$BACKUP_DIR" -name "Caddyfile.*" -mtime +30 -delete

echo "Caddy backup completed: $TIMESTAMP"

Schedule with cron:

crontab -e
# Add: 0 2 * * * /opt/caddy/backup-caddy.sh

Troubleshooting

Common Issues

Issue: Certificate acquisition fails

Check DNS resolution:

# Verify A record
dig example.com

# Test ACME challenge
curl -I http://example.com/.well-known/acme-challenge/test

# Check Caddy logs
journalctl -u caddy -n 100

Issue: Service not accessible

Verify configuration:

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

# Check Caddy is running
systemctl status caddy

# View current config
caddy adapt --config /etc/caddy/Caddyfile --pretty

# Test manually
curl -v http://localhost:80

Issue: WebSocket connections failing

Ensure proper headers:

websocket-app.example.com {
    reverse_proxy localhost:8080 {
        header_up Upgrade {>Upgrade}
        header_up Connection {>Connection}
    }
}

Logging and Debugging

Enable debug logging:

{
    log {
        level DEBUG
    }
}

example.com {
    log {
        output file /var/log/caddy/example.com.log {
            roll_size 100mb
        }
        format json
        level DEBUG
    }

    reverse_proxy localhost:8080
}

View logs:

# System logs
journalctl -u caddy -f

# Caddy logs
tail -f /var/log/caddy/caddy.log

# Docker logs
docker logs -f caddy

Conclusion

Caddy provides a modern, simple, and secure solution for reverse proxying homelab services. With automatic HTTPS, intuitive configuration, and excellent Docker integration, Caddy removes the complexity traditionally associated with web servers and reverse proxies.

Key Takeaways

  • Automatic HTTPS makes SSL certificates effortless
  • Simple configuration reduces deployment time
  • Docker integration works seamlessly with containerized services
  • Modern features like HTTP/3 come built-in
  • AI automation can enhance configuration and monitoring

Next Steps

Immediate actions:

  • Deploy Caddy in your homelab
  • Configure 2-3 services behind Caddy
  • Set up automatic HTTPS
  • Implement security headers

Advanced exploration:

  • Build custom Caddy with modules
  • Implement load balancing
  • Create AI-powered monitoring
  • Integrate with infrastructure-as-code tools

Additional Resources


Document Version: 1.0 Last Updated: December 16, 2025 Difficulty: Beginner to Intermediate Estimated Completion Time: 1-2 hours Maintenance Required: Minimal (automatic certificate renewal)