Caddy Web Server Guide
Table of Contents
- Introduction
- Key Features
- Installation
- Basic Configuration
- Caddyfile Syntax
- HTTPS and Automatic TLS
- Reverse Proxy
- Static File Serving
- API Usage
- Performance Optimization
- Advanced Security Configuration
- Plugins and Extending Caddy
- Docker Integration
- Scaling Caddy
- Monitoring and Logging
- Troubleshooting
- Real-world Examples
- Migration from Other Web Servers
- Caddy vs. Alternatives
- 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:
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
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
|
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
- Certificate issues:
1
2
3
4
5
| example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
}
|
This uses DNS validation when HTTP validation fails.
- Permission problems:
1
2
| # Give Caddy permission to bind to privileged ports
sudo setcap cap_net_bind_service=+ep /usr/bin/caddy
|
- 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
Feature | Caddy | Nginx | Apache | Traefik |
---|
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.