Caddy: The Automatic HTTPS Web Server
Zero-config HTTPS with automatic certificate management. Modern, secure web serving for your homelab.
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:
- Serves traffic for example.com
- Automatically obtains SSL certificate
- Redirects HTTP to HTTPS
- Proxies requests to localhost:8080
- Automatically renews the certificate
Automatic HTTPS
Caddy’s killer feature is zero-configuration HTTPS:
How it works:
- Caddy detects domain names in your configuration
- Contacts Let’s Encrypt to obtain certificates
- Completes ACME challenge (HTTP-01 or TLS-ALPN-01)
- Serves traffic with HTTPS enabled
- 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
Method 1: Binary Installation (Recommended)
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
| Feature | Caddy | Nginx |
|---|---|---|
| Configuration | Simple Caddyfile | Complex nginx.conf |
| Automatic HTTPS | Yes (built-in) | No (requires certbot) |
| HTTP/2 | Automatic | Manual config |
| HTTP/3 | Built-in | Requires recompile |
| Certificate Renewal | Automatic | Manual/cron job |
| Learning Curve | Easy | Moderate to difficult |
| Performance | Excellent | Excellent |
| Memory Usage | ~20-50MB | ~5-20MB |
| Ecosystem | Growing | Mature |
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
| Feature | Caddy | Traefik |
|---|---|---|
| Configuration | Caddyfile or JSON | Labels or YAML |
| Automatic HTTPS | Yes | Yes |
| Docker Integration | Good | Excellent |
| Kubernetes Support | Plugin | Native |
| Dashboard | Optional | Built-in |
| Learning Curve | Easy | Moderate |
| Dynamic Config | Reload needed | Automatic |
| Resource Usage | Lower | Higher |
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
| Feature | Caddy | NPM |
|---|---|---|
| Interface | CLI/Config files | Web GUI |
| Configuration | Text-based | Click-based |
| Automatic HTTPS | Yes | Yes |
| Flexibility | Very high | Limited by GUI |
| Learning Curve | Easy | Very easy |
| Version Control | Easy (text files) | Difficult |
| Automation | Excellent | Limited |
| Resource Usage | Lower | Higher (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
- Official Caddy Documentation: https://caddyserver.com/docs/
- Caddy Community Forum: https://caddy.community/
- GitHub Repository: https://github.com/caddyserver/caddy
- Caddy Modules: https://caddyserver.com/download
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)