TL;DR
Caddy is a modern, Go-based web server that provides automatic HTTPS via Let's Encrypt with zero configuration. It serves as a drop-in replacement for Nginx and Apache with a dramatically simpler config syntax (Caddyfile). Caddy handles TLS certificate provisioning, renewal, and OCSP stapling automatically. It supports reverse proxying, load balancing, static file serving, HTTP/3, and dynamic configuration via its admin API. For most web serving use cases in 2026, Caddy offers the best developer experience with production-grade reliability.
Key Takeaways
- Caddy automatically provisions and renews HTTPS certificates from Let's Encrypt β no certbot, no cron jobs, no manual renewal
- The Caddyfile syntax is dramatically simpler than Nginx/Apache configs: a basic reverse proxy is just 3 lines
- Caddy supports HTTP/3 (QUIC), on-demand TLS, wildcard certificates, and the ACME protocol out of the box
- Caddy's admin API allows live configuration changes without restarts or reloads
- Caddy excels as a Docker reverse proxy with automatic HTTPS for containerized microservices
- Migration from Nginx to Caddy typically reduces configuration complexity by 60-80%
Caddy is an open-source web server written in Go that has gained significant traction as a modern alternative to Nginx and Apache. Its standout feature is automatic HTTPS: Caddy obtains and renews TLS certificates from Let's Encrypt (or ZeroSSL) without any configuration. In 2026, Caddy powers millions of sites and has become the default choice for developers who want production-grade web serving without the complexity of traditional server configuration. This guide covers everything from basic setup to advanced production deployment patterns.
What Is Caddy and Why It Matters
Caddy is a powerful, extensible web server platform written in Go. It was created by Matt Holt in 2015 and has since grown into a mature, production-ready server used by companies like Cloudflare, Ardan Labs, and thousands of startups. Caddy v2 (the current major version) was a complete rewrite that introduced a modular architecture, a JSON config system, and the Caddyfile adapter.
What makes Caddy unique is its philosophy of secure defaults. HTTPS is automatic and on by default. HTTP requests are automatically redirected to HTTPS. OCSP stapling is enabled. Modern TLS versions are enforced. Headers are secure by default. This means a fresh Caddy installation with zero configuration is already more secure than most manually configured Nginx or Apache setups.
- Automatic HTTPS via Let's Encrypt and ZeroSSL with zero configuration
- Written in Go β single static binary, no dependencies, cross-platform
- Caddyfile: human-readable configuration that is dramatically simpler than Nginx or Apache
- Native HTTP/3 (QUIC) support enabled by default
- Admin API for live configuration changes without downtime
- Extensible module system β add functionality without forking
- Graceful reloads, automatic certificate management, and OCSP stapling
Caddy vs Nginx vs Apache vs Traefik
Each web server has strengths suited to different use cases. This comparison helps you decide which is right for your project.
| Feature | Caddy | Nginx | Apache | Traefik |
|---|---|---|---|---|
| Automatic HTTPS | Built-in (zero config) | Manual (certbot) | Manual (certbot) | Built-in (config needed) |
| Config syntax | Caddyfile (simple) | nginx.conf (complex) | httpd.conf + .htaccess | YAML/TOML/labels |
| Raw performance | Very good | Excellent | Good | Good |
| Memory usage | Low (~20MB) | Very low (~5MB) | High (~50-200MB) | Low (~30MB) |
| HTTP/3 support | Native (default on) | Experimental | Not supported | Experimental |
| Docker integration | Good (labels/networks) | Manual config | Manual config | Excellent (native) |
| Plugin system | Go modules | C modules (compile) | Dynamic modules | Go plugins |
| Learning curve | Easy | Moderate | Moderate-Hard | Moderate |
| Written in | Go | C | C | Go |
| License | Apache 2.0 | BSD 2-Clause | Apache 2.0 | MIT |
Installation
Caddy distributes as a single static binary. No runtime dependencies are needed.
Package Managers
# Debian / Ubuntu
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
# Fedora / RHEL
dnf copr enable @caddy/caddy && dnf install caddy
# macOS
brew install caddy
# Windows
choco install caddyDocker
# Pull official Caddy image
docker pull caddy:latest
# Run with Caddyfile
docker run -d --name caddy \
-p 80:80 -p 443:443 -p 443:443/udp \
-v $PWD/Caddyfile:/etc/caddy/Caddyfile \
-v caddy_data:/data \
-v caddy_config:/config \
caddy:latestDirect Binary Download
# Download from GitHub releases
curl -OL https://github.com/caddyserver/caddy/releases/latest/download/caddy_2.9.1_linux_amd64.tar.gz
tar xzf caddy_2.9.1_linux_amd64.tar.gz
sudo mv caddy /usr/bin/caddy
sudo chmod +x /usr/bin/caddy
# Verify installation
caddy versionBuilding from Source with xcaddy
# Install xcaddy (Caddy build tool)
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
# Build Caddy with custom plugins
xcaddy build \
--with github.com/caddy-dns/cloudflare \
--with github.com/mholt/caddy-ratelimit \
--with github.com/caddyserver/transform-encoderCaddyfile Basics
The Caddyfile is Caddy's human-friendly configuration format. It is designed to be easy to read and write, even for complex setups.
Automatic HTTPS (Default Behavior)
When you specify a domain name in your Caddyfile, Caddy automatically obtains a TLS certificate from Let's Encrypt, redirects HTTP to HTTPS, and enables OCSP stapling. No additional configuration is needed.
# Minimal Caddyfile β automatic HTTPS!
# Just specify your domain and Caddy does the rest
example.com {
respond "Hello, world!"
}
# What Caddy does automatically:
# 1. Obtains TLS certificate from Let's Encrypt
# 2. Redirects http://example.com β https://example.com
# 3. Enables OCSP stapling
# 4. Enforces modern TLS (1.2+)
# 5. Renews certificate before expiryStatic File Server
# Serve static files with compression and caching
example.com {
root * /var/www/html
encode gzip zstd
file_server
# Cache static assets for 1 year
@static path *.css *.js *.png *.jpg *.svg *.woff2
header @static Cache-Control "public, max-age=31536000, immutable"
# Cache HTML for 1 hour
@html path *.html
header @html Cache-Control "public, max-age=3600"
}Redirects
# Redirect www to non-www
www.example.com {
redir https://example.com{uri} permanent
}
# Redirect old paths
example.com {
redir /old-page /new-page permanent
redir /blog/old-slug /blog/new-slug 301
}Reverse Proxy Configuration
Caddy is an excellent reverse proxy. It supports single backends, load balancing, health checks, WebSocket proxying, and header manipulation.
Single Backend
# Simple reverse proxy β 3 lines!
app.example.com {
reverse_proxy localhost:3000
}
# With header manipulation
api.example.com {
reverse_proxy localhost:8080 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
header_down -Server
}
}Load Balancing
# Load balancing across multiple backends
app.example.com {
reverse_proxy {
to localhost:3001
to localhost:3002
to localhost:3003
lb_policy round_robin
# Other policies: random, first, ip_hash,
# uri_hash, least_conn, header
}
}Health Checks
# Active health checks
app.example.com {
reverse_proxy {
to localhost:3001
to localhost:3002
health_uri /health
health_interval 10s
health_timeout 5s
health_status 200
# Passive health checks (circuit breaker)
fail_duration 30s
max_fails 3
unhealthy_latency 500ms
}
}WebSocket Proxying
# WebSocket proxying β Caddy handles upgrade automatically
ws.example.com {
reverse_proxy localhost:8080
# That's it! Caddy detects WebSocket upgrades
# and handles them transparently.
}
# With path-based routing
example.com {
reverse_proxy /ws/* localhost:8080
reverse_proxy /api/* localhost:3000
file_server
}Nginx Equivalent Comparison
Here is the same reverse proxy configuration in both Caddy and Nginx for comparison.
# CADDY β Reverse proxy with HTTPS (3 lines)
app.example.com {
reverse_proxy localhost:3000
}
# NGINX equivalent (~25 lines + certbot + cron)
# server { listen 80; server_name app.example.com;
# return 301 https://$server_name$request_uri; }
# server { listen 443 ssl http2;
# server_name app.example.com;
# ssl_certificate /etc/letsencrypt/live/.../fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/.../privkey.pem;
# location / {
# proxy_pass http://localhost:3000;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# }
# } + certbot setup + renewal cron jobAutomatic HTTPS Deep Dive
Caddy's automatic HTTPS is its most compelling feature. It handles the entire TLS lifecycle automatically.
- Obtains certificates from Let's Encrypt (or ZeroSSL) using the ACME protocol
- Automatically renews certificates before they expire (default: 30 days before expiry)
- Redirects HTTP to HTTPS automatically
- Enables OCSP stapling for faster TLS handshakes
- Supports ACME DNS challenge for wildcard certificates
- On-demand TLS: obtain certificates at handshake time for dynamic domains
Wildcard Certificates
Caddy supports wildcard certificates using the DNS challenge. You need a DNS provider plugin for your registrar.
# Wildcard certificate with Cloudflare DNS
# Build Caddy with: xcaddy build --with github.com/caddy-dns/cloudflare
*.example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
@app host app.example.com
handle @app {
reverse_proxy localhost:3000
}
@api host api.example.com
handle @api {
reverse_proxy localhost:8080
}
handle {
respond "Unknown subdomain" 404
}
}On-Demand TLS
On-demand TLS obtains certificates during the TLS handshake. This is useful for SaaS platforms where customer domains are not known in advance.
# On-demand TLS for SaaS custom domains
{
on_demand_tls {
ask http://localhost:5555/check-domain
interval 5m
burst 10
}
}
https:// {
tls {
on_demand
}
reverse_proxy localhost:3000
}Docker Deployment
Caddy works exceptionally well in Docker environments. Here are common deployment patterns.
Basic Docker Setup
# Dockerfile for custom Caddy
FROM caddy:2-builder AS builder
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare \
--with github.com/mholt/caddy-ratelimit
FROM caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
COPY Caddyfile /etc/caddy/CaddyfileDocker Compose Multi-Service
# docker-compose.yml β Multi-service with Caddy
services:
caddy:
image: caddy:latest
restart: unless-stopped
ports: ["80:80", "443:443", "443:443/udp"]
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
networks: [web]
app:
build: ./app
expose: ["3000"]
networks: [web]
api:
build: ./api
expose: ["8080"]
networks: [web, backend]
db:
image: postgres:16-alpine
volumes: [pgdata:/var/lib/postgresql/data]
networks: [backend]
volumes: { caddy_data: {}, pgdata: {} }
networks: { web: {}, backend: {} }
# Caddyfile for the compose setup
app.example.com { reverse_proxy app:3000 }
api.example.com { reverse_proxy api:8080 }PHP and WordPress with Caddy
Caddy has first-class PHP support through the php_fastcgi directive, which handles path splitting, index file resolution, and FastCGI communication.
Basic PHP Setup
# Basic PHP with Caddy
example.com {
root * /var/www/html
encode gzip
php_fastcgi unix//run/php/php8.3-fpm.sock
file_server
}WordPress Configuration
# WordPress with Caddy β complete config
example.com {
root * /var/www/wordpress
encode gzip
# PHP processing
php_fastcgi unix//run/php/php8.3-fpm.sock
# Static file serving
file_server
# Block access to sensitive files
@blocked path /xmlrpc.php /wp-config.php
respond @blocked 403
# Cache static assets
@static path *.css *.js *.png *.jpg *.gif *.ico *.svg *.woff2
header @static Cache-Control "public, max-age=31536000"
# Security headers
header {
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy strict-origin-when-cross-origin
-Server
}
}SPA Hosting (React, Vue, Next.js)
Caddy handles single-page applications elegantly with the try_files directive for client-side routing fallback.
# React / Vue SPA hosting
app.example.com {
root * /var/www/app/dist
encode gzip zstd
# SPA client-side routing fallback
try_files {path} /index.html
file_server
# Proxy API requests to backend
reverse_proxy /api/* localhost:8080
# Cache static assets aggressively
@static path *.js *.css *.png *.svg *.woff2
header @static Cache-Control "public, max-age=31536000, immutable"
}
# Next.js / Nuxt.js (SSR mode)
ssr.example.com {
reverse_proxy localhost:3000
}Caddy as API Gateway
Caddy can function as an API gateway, routing requests to different backend services based on path prefixes, with rate limiting and CORS headers.
# Caddy as API Gateway
api.example.com {
# CORS headers
header Access-Control-Allow-Origin "https://app.example.com"
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
header Access-Control-Allow-Headers "Authorization, Content-Type"
# Route to different microservices
reverse_proxy /users/* user-service:3001
reverse_proxy /orders/* order-service:3002
reverse_proxy /payments/* payment-service:3003
reverse_proxy /notifications/* notification-service:3004
# Health check endpoint
respond /health 200
}HTTP/3 and QUIC Support
Caddy supports HTTP/3 (QUIC) natively and enables it by default. HTTP/3 uses UDP instead of TCP, providing faster connection establishment, better performance on lossy networks, and eliminates head-of-line blocking. No additional configuration is needed β Caddy advertises HTTP/3 support via the Alt-Svc header automatically.
# HTTP/3 is enabled by default in Caddy!
# Make sure to expose UDP port 443:
# docker run -p 443:443/udp ...
# To explicitly disable HTTP/3:
{
servers {
protocols h1 h2
# Omitting h3 disables QUIC
}
}
# Response headers will include:
# Alt-Svc: h3=":443"; ma=2592000Caddy JSON Config vs Caddyfile
Caddy has two configuration interfaces: the Caddyfile (human-friendly) and JSON (machine-friendly). The Caddyfile is actually an adapter that converts to JSON internally. The JSON config provides full control over every Caddy feature and is ideal for programmatic configuration.
# Convert Caddyfile to JSON (see what Caddy generates internally)
caddy adapt --config Caddyfile --pretty
# JSON config gives full control β ideal for programmatic configs
# { "apps": { "http": { "servers": { "srv0": {
# "listen": [":443"],
# "routes": [{ "match": [{"host": ["app.example.com"]}],
# "handle": [{"handler": "reverse_proxy",
# "upstreams": [{"dial": "localhost:3000"}]}] }]
# }}}}}Caddy Admin API
Caddy exposes a REST API on localhost:2019 for live configuration management. You can load, modify, and inspect configuration without restarting the server.
# Caddy Admin API (localhost:2019)
# Load new JSON config
curl localhost:2019/load -H "Content-Type: application/json" -d @caddy.json
# Load Caddyfile via adapter
curl localhost:2019/load -H "Content-Type: text/caddyfile" --data-binary @Caddyfile
# Get current config
curl localhost:2019/config/
# Graceful reload from CLI
caddy reload --config /etc/caddy/CaddyfileRate Limiting and Security Headers
Caddy provides built-in security features and can be extended with rate limiting modules.
# Security headers and rate limiting
example.com {
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy "camera=(), microphone=(), geolocation=()"
-Server
}
# Basic authentication
basicauth /admin/* {
admin $2a$14$hashed_password_here
}
# IP allowlist
@blocked not remote_ip 10.0.0.0/8
respond @blocked /internal/* 403
reverse_proxy localhost:3000
}Logging and Metrics
Caddy produces structured JSON logs by default, which are easy to parse with tools like jq, Loki, or Elasticsearch. It also supports Prometheus metrics via a module.
# Structured JSON logging with rotation
example.com {
log {
output file /var/log/caddy/access.log {
roll_size 100MiB
roll_keep 10
}
format json
}
reverse_proxy localhost:3000
}
# Prometheus metrics: built-in at :2019/metricsPerformance Tuning
Caddy performs well out of the box, but can be tuned for high-traffic scenarios.
- Enable static file compression with encode gzip zstd
- Use file_server with precompressed to serve pre-compressed files
- Tune worker count with GOMAXPROCS environment variable
- Enable HTTP/3 for better performance on mobile and lossy networks
- Use header caching directives for static assets
- Configure connection keep-alive timeouts appropriately
# Performance-optimized Caddyfile
example.com {
root * /var/www/html
# Enable compression (gzip + zstd)
encode zstd gzip
# Serve pre-compressed files if available
file_server {
precompressed zstd gzip br
}
# Aggressive caching for static assets
@immutable path *.js *.css *.woff2 *.png *.jpg *.svg
header @immutable {
Cache-Control "public, max-age=31536000, immutable"
Vary Accept-Encoding
}
# Keep-alive settings
# (Caddy defaults are good for most cases)
}Caddy Modules and Plugins
Caddy has a rich plugin ecosystem. Plugins are Go modules that extend Caddy's functionality. You build a custom Caddy binary with the plugins you need using xcaddy.
# Popular Caddy modules (install with xcaddy)
xcaddy build \
--with github.com/caddy-dns/cloudflare \ # DNS challenge (Cloudflare)
--with github.com/caddy-dns/route53 \ # DNS challenge (AWS)
--with github.com/mholt/caddy-ratelimit \ # Rate limiting
--with github.com/caddyserver/cache-handler # HTTP caching
# Prometheus metrics β built-in since v2.7 (no plugin needed)
# List installed modules
caddy list-modulesMigration from Nginx to Caddy
Migrating from Nginx to Caddy typically simplifies your configuration dramatically. Here are common Nginx patterns and their Caddy equivalents.
# Migration cheat sheet: Nginx β Caddy
#
# server { listen 443 ssl; server_name X; } β X { }
# location / { proxy_pass http://...; } β reverse_proxy localhost:3000
# location ~ \.php$ { fastcgi_pass ...; } β php_fastcgi unix//run/php/php8.3-fpm.sock
# try_files $uri $uri/ /index.html β try_files {path} /index.html
# rewrite ^/old$ /new permanent β redir /old /new permanent
# add_header X-Frame-Options DENY β header X-Frame-Options DENY
# ssl_certificate + certbot + cron β (automatic β nothing needed)
# gzip on; gzip_types ... β encode gzip zstdProduction Deployment Best Practices
- Run Caddy as a systemd service for automatic restart and logging
- Use the Caddyfile for human-managed configs, JSON for automation
- Store Caddy data directory (/data) on persistent storage for certificates
- Set up log rotation for access and error logs
- Monitor certificate expiry with Prometheus metrics
- Use the admin API endpoint only on localhost (default)
- Keep Caddy updated β security patches are released regularly
- Test configuration changes with caddy validate before applying
# Install as systemd service (package managers do this automatically)
sudo systemctl enable --now caddy
# Validate config before applying
caddy validate --config /etc/caddy/Caddyfile
# Graceful reload (no downtime)
sudo systemctl reload caddy
# Check status and logs
sudo systemctl status caddy
journalctl -u caddy --no-pager -fCommon Issues and Troubleshooting
- Certificate not obtained: Check that ports 80 and 443 are accessible from the internet. Caddy needs these for the ACME HTTP challenge.
- Permission denied on port 80/443: On Linux, run setcap cap_net_bind_service=+ep /usr/bin/caddy or use systemd socket activation.
- Too many certificates: Let's Encrypt has rate limits (50 certificates per registered domain per week). Use staging for testing.
- Caddy not reloading config: Use caddy reload instead of restarting. Check caddy validate first.
- Reverse proxy 502 errors: Verify the backend is running and accessible. Check if the backend expects specific Host headers.
- WebSocket not working through proxy: Caddy handles WebSocket upgrades automatically. Check if your backend requires specific headers.
- High memory usage: Check for excessive logging. Caddy's memory usage scales with the number of active connections and certificates.
- Slow certificate issuance: DNS propagation can be slow for DNS challenges. Use the HTTP challenge when possible.
Frequently Asked Questions
Is Caddy ready for production use?
Yes. Caddy v2 has been production-ready since 2020 and is used by thousands of companies. It handles automatic HTTPS, graceful reloads, and has been extensively battle-tested. Companies like Cloudflare, Fly.io, and Replit use Caddy in production.
How does Caddy compare to Nginx in performance?
For most real-world workloads, Caddy and Nginx perform similarly. Nginx has a slight edge in raw static file throughput due to its C implementation, but Caddy's Go-based architecture is more than fast enough for the vast majority of use cases. The difference is typically less than 10% and is negligible compared to backend processing time.
Can Caddy replace Nginx as a reverse proxy?
Absolutely. Caddy excels as a reverse proxy with automatic HTTPS, load balancing, health checks, and WebSocket support. The configuration is dramatically simpler than Nginx. Many teams have migrated from Nginx to Caddy specifically for the reverse proxy use case.
Does Caddy support wildcard certificates?
Yes. Caddy supports wildcard certificates using the ACME DNS challenge. You need a DNS provider plugin for your registrar (Cloudflare, Route53, Google Cloud DNS, etc.). The configuration is straightforward and certificates are automatically renewed.
How does Caddy handle certificate renewal?
Caddy automatically renews certificates 30 days before expiry. It uses the same ACME challenge type that was used for initial issuance. If renewal fails, Caddy retries with exponential backoff and logs warnings. Certificates are stored in the Caddy data directory and persist across restarts.
Can I use Caddy with Docker and Kubernetes?
Yes. Caddy has an official Docker image and works well in container environments. For Kubernetes, you can use Caddy as an ingress controller or as a sidecar proxy. The Docker Compose pattern with Caddy as the entry point for multiple services is very popular.
Is Caddy free and open source?
Yes. Caddy is licensed under the Apache 2.0 license and is completely free to use, including for commercial purposes. There are no paid tiers or enterprise editions. The full source code is on GitHub.
How do I migrate from Nginx to Caddy?
Start by translating your nginx.conf to a Caddyfile. Most Nginx directives have direct Caddy equivalents but with simpler syntax. Remove all SSL/TLS configuration (Caddy handles it automatically). Test with caddy validate, then switch DNS. The migration typically reduces config complexity by 60-80%.