Caddy is a modern, open-source web server written in Go that stands out for one killer feature: automatic HTTPS. While traditional web servers like Nginx and Apache require manual certificate setup with Certbot or similar tools, Caddy obtains and renews TLS certificates from Let’s Encrypt the moment you point a domain at it. Combined with its minimal configuration syntax, built-in reverse proxy, and HTTP/3 support, Caddy has become the go-to choice for developers who want secure, production-ready web serving with the least possible effort.
What Is Caddy?
Caddy is an extensible web server platform that prioritizes ease of use and secure defaults. It was created by Matt Holt in 2015 and is distributed as a single, statically-compiled binary with no external dependencies.
Key characteristics of Caddy:
- Automatic HTTPS — Provisions and renews TLS certificates from Let’s Encrypt or ZeroSSL without any configuration
- HTTP/2 and HTTP/3 — Enabled by default for all HTTPS sites
- Reverse proxy — Built-in reverse proxy with load balancing, health checks, and header manipulation
- Zero-config mode — A simple two-line Caddyfile can serve an entire site with HTTPS
- Cross-platform — Runs on Linux, macOS, Windows, and BSD
- Plugin ecosystem — Extend functionality with modules for DNS providers, authentication, rate limiting, and more
Caddy is distributed under the Apache 2.0 license and is free for both personal and commercial use.
Caddy vs Nginx
If you are currently using Nginx and wondering whether Caddy is right for your project, here is a direct comparison:
| Feature | Caddy | Nginx |
|---|---|---|
| HTTPS | Automatic (built-in ACME client) | Manual (requires Certbot or similar) |
| Configuration syntax | Caddyfile (minimal, human-readable) | nginx.conf (powerful but verbose) |
| HTTP/2 | Enabled by default | Requires explicit configuration |
| HTTP/3 (QUIC) | Built-in, enabled by default | Experimental (requires separate build) |
| Reverse proxy | Built-in directive | Built-in module |
| Load balancing | Built-in with multiple policies | Built-in (round-robin, least_conn, etc.) |
| Configuration reload | Zero-downtime via API or SIGHUP | Zero-downtime via nginx -s reload |
| Language | Go (memory-safe) | C (high performance) |
| Memory usage | Low (~20-50 MB) | Very low (~5-15 MB) |
| Raw throughput | Very good | Excellent (handles millions of RPS) |
| Community & ecosystem | Growing rapidly | Massive, decades of documentation |
When to choose Caddy: You want automatic HTTPS, minimal configuration, and a modern feature set without manual certificate management. Ideal for self-hosted applications, personal projects, and small-to-medium deployments.
When to choose Nginx: You need extremely granular control, maximum raw throughput for millions of concurrent connections, or extensive module compatibility from decades of ecosystem development.
Prerequisites
Before installing Caddy, make sure you have:
- A Linux server running Ubuntu 22.04+ or Debian 12+ (other distributions also supported)
- A domain name with DNS A/AAAA records pointing to your server’s public IP address
- Ports 80 and 443 open in your firewall (required for ACME HTTP challenge and HTTPS)
- Root or sudo access to the server
- No other web server (Nginx, Apache) already listening on ports 80/443
Verify your firewall allows the required ports:
# If using UFW
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status
Installing Caddy on Ubuntu
The recommended installation method uses the official Caddy APT repository, which provides automatic updates:
# Install required dependencies
sudo apt update
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
# Add Caddy's GPG key
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
# Add the Caddy repository
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 -y caddy
Verify the installation:
caddy version
You should see output like v2.8.4 h1:... confirming Caddy is installed. The package installation also creates a systemd service, a default Caddyfile at /etc/caddy/Caddyfile, and a caddy user for running the process.
Alternative: Install from Binary
If you prefer a manual installation or need a specific version:
# Download the latest release
curl -Lo caddy.tar.gz "https://github.com/caddyserver/caddy/releases/latest/download/caddy_2.8.4_linux_amd64.tar.gz"
# Extract and move to PATH
tar xzf caddy.tar.gz
sudo mv caddy /usr/bin/caddy
sudo chmod +x /usr/bin/caddy
# Verify
caddy version
Understanding the Caddyfile
The Caddyfile is Caddy’s configuration file. Its syntax is intentionally minimal — you describe what you want, not how to achieve it. Caddy fills in the sensible defaults.
The Caddyfile lives at /etc/caddy/Caddyfile when installed from the package manager. Here is the basic structure:
# Global options (optional)
{
email admin@example.com
}
# Site block
example.com {
root * /var/www/html
file_server
}
Key concepts:
- Site address — The domain or IP before the opening brace. Using a domain name triggers automatic HTTPS.
- Directives — Commands inside the site block like
root,file_server,reverse_proxy. - Global options — Settings inside a top-level
{}block without an address. Used for email (for ACME registration), logging defaults, etc. - Matchers — Patterns like
*or/api/*that control which requests a directive applies to.
After editing the Caddyfile, validate and reload:
# Validate syntax
caddy validate --config /etc/caddy/Caddyfile
# Reload without downtime
sudo systemctl reload caddy
Serving Static Files
Serving a static website with Caddy requires just a few lines:
example.com {
root * /var/www/mysite
file_server
}
That is it. Caddy will:
- Obtain a TLS certificate for
example.comautomatically - Serve files from
/var/www/mysite - Enable HTTP/2 and HTTP/3
- Redirect HTTP to HTTPS
For a more complete static site configuration with compression and caching:
example.com {
root * /var/www/mysite
# Enable gzip and zstd compression
encode gzip zstd
# Serve static files with directory browsing disabled
file_server {
hide .git .env
}
# Custom error pages
handle_errors {
rewrite * /{err.status_code}.html
file_server
}
# Cache static assets
@static path *.css *.js *.png *.jpg *.gif *.svg *.woff2
header @static Cache-Control "public, max-age=2592000, immutable"
# Security headers
header {
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
Create the web root and a test page:
sudo mkdir -p /var/www/mysite
echo '<h1>Hello from Caddy</h1>' | sudo tee /var/www/mysite/index.html
sudo chown -R caddy:caddy /var/www/mysite
Automatic HTTPS
Automatic HTTPS is Caddy’s signature feature. Understanding how it works helps you troubleshoot and customize the behavior.
How It Works
When Caddy encounters a site block with a public domain name (not localhost or an IP), it automatically:
- Checks DNS — Verifies the domain resolves to the server’s public IP
- Requests a certificate — Contacts Let’s Encrypt (or ZeroSSL as fallback) via the ACME protocol
- Completes the HTTP-01 challenge — Proves domain ownership by serving a token on port 80
- Installs the certificate — Configures TLS with the obtained certificate and key
- Redirects HTTP to HTTPS — Creates an automatic redirect on port 80
- Schedules renewal — Renews the certificate before expiration (typically 30 days ahead)
The ACME Protocol
ACME (Automatic Certificate Management Environment) is the protocol that Let’s Encrypt uses to verify domain ownership. Caddy includes a full ACME client that supports:
- HTTP-01 challenge — Serves a token file via HTTP on port 80 (default)
- TLS-ALPN-01 challenge — Uses TLS negotiation on port 443
- DNS-01 challenge — Creates a DNS TXT record (requires a DNS provider plugin)
Configuring the ACME Email
Set an email address for certificate expiration notifications and account registration:
{
email admin@example.com
}
example.com {
reverse_proxy localhost:3000
}
Using a Staging CA for Testing
During development, use Let’s Encrypt’s staging environment to avoid rate limits:
{
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
example.com {
reverse_proxy localhost:3000
}
Important: Staging certificates are not trusted by browsers. Remove the
acme_cadirective when moving to production.
Internal (Self-Signed) Certificates
For local development or internal services that do not need public certificates:
{
local_certs
}
localhost {
reverse_proxy localhost:3000
}
Caddy will generate a self-signed certificate and install its root CA into the system trust store so browsers accept it locally.
Reverse Proxy Configuration
Caddy’s reverse_proxy directive provides a full-featured reverse proxy with minimal configuration.
Basic Reverse Proxy
app.example.com {
reverse_proxy localhost:3000
}
This single line proxies all traffic from app.example.com to a backend running on port 3000, with automatic HTTPS, HTTP/2, and proper header forwarding.
Multiple Backends on One Domain
Use path-based routing to proxy different paths to different backends:
example.com {
reverse_proxy /api/* localhost:8000
reverse_proxy /admin/* localhost:9000
# Everything else serves static files
root * /var/www/frontend
file_server
}
Multiple Domains (Virtual Hosts)
app.example.com {
reverse_proxy localhost:3000
}
api.example.com {
reverse_proxy localhost:8000
}
admin.example.com {
reverse_proxy localhost:9000 {
header_up X-Custom-Header "admin-panel"
}
}
Each domain automatically gets its own TLS certificate.
Preserving Client Information
Caddy automatically sets X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host headers. You can add or override headers:
app.example.com {
reverse_proxy localhost:3000 {
header_up Host {upstream_hostport}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Port {server_port}
}
}
WebSocket Support
Caddy proxies WebSocket connections transparently — no additional configuration needed:
ws.example.com {
reverse_proxy localhost:4000
}
The Upgrade and Connection headers are handled automatically by Caddy’s reverse proxy.
Load Balancing
Caddy supports load balancing across multiple backend instances with various policies.
Round-Robin (Default)
app.example.com {
reverse_proxy localhost:3001 localhost:3002 localhost:3003
}
Load Balancing Policies
app.example.com {
reverse_proxy localhost:3001 localhost:3002 localhost:3003 {
lb_policy least_conn
}
}
Available policies:
| Policy | Description |
|---|---|
random | Choose a random backend |
least_conn | Send to the backend with fewest active connections |
round_robin | Cycle through backends sequentially (default) |
first | Always use the first available backend |
ip_hash | Route based on client IP for session affinity |
uri_hash | Route based on request URI |
header | Route based on a request header value |
cookie | Route based on a cookie value for session persistence |
Health Checks
Enable active health checks to detect and remove unhealthy backends:
app.example.com {
reverse_proxy localhost:3001 localhost:3002 localhost:3003 {
lb_policy least_conn
health_uri /health
health_interval 10s
health_timeout 5s
health_status 200
# Passive health checks
fail_duration 30s
max_fails 3
unhealthy_latency 500ms
}
}
Headers, Compression, and Caching
Custom Response Headers
example.com {
header {
# Security headers
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=()"
# Remove server identification
-Server
# Cache control for dynamic content
Cache-Control "no-store, no-cache, must-revalidate"
}
reverse_proxy localhost:3000
}
Compression
Enable transparent compression with encode:
example.com {
encode zstd gzip
reverse_proxy localhost:3000
}
Caddy automatically negotiates the best compression algorithm based on the client’s Accept-Encoding header. Zstandard (zstd) is preferred when supported, as it provides better compression ratios and faster decompression than gzip.
Static Asset Caching
example.com {
@static path *.css *.js *.png *.jpg *.gif *.svg *.woff2 *.ico
header @static Cache-Control "public, max-age=31536000, immutable"
@dynamic not path *.css *.js *.png *.jpg *.gif *.svg *.woff2 *.ico
header @dynamic Cache-Control "no-cache, must-revalidate"
reverse_proxy localhost:3000
}
Running Caddy as a systemd Service
The APT package installation creates a systemd service automatically. Here are the essential commands:
# Start Caddy
sudo systemctl start caddy
# Stop Caddy
sudo systemctl stop caddy
# Restart Caddy (brief downtime)
sudo systemctl restart caddy
# Reload configuration without downtime
sudo systemctl reload caddy
# Enable Caddy to start on boot
sudo systemctl enable caddy
# Check service status
sudo systemctl status caddy
# View logs
sudo journalctl -u caddy --no-pager -f
The Caddy Service File
The default systemd unit file is located at /lib/systemd/system/caddy.service. It runs Caddy as the caddy user and loads /etc/caddy/Caddyfile:
[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target
[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
Note: If you installed Caddy manually (not from the package), you need to create this service file yourself and add the
caddyuser withsudo useradd --system --home /var/lib/caddy --shell /usr/sbin/nologin caddy.
Verifying the Installation
After starting Caddy, verify it is serving your site:
# Check Caddy is listening
sudo ss -tlnp | grep caddy
# Test HTTPS (replace with your domain)
curl -I https://example.com
# Check certificate details
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -dates
Caddyfile Directives Reference
Here is a reference of the most commonly used Caddyfile directives:
| Directive | Purpose | Example |
|---|---|---|
reverse_proxy | Proxy requests to backend servers | reverse_proxy localhost:3000 |
file_server | Serve static files from disk | file_server |
root | Set the document root directory | root * /var/www/html |
encode | Enable response compression | encode gzip zstd |
header | Set, add, or remove response headers | header X-Frame-Options "DENY" |
redir | Redirect requests to a new URL | redir /old /new permanent |
rewrite | Rewrite the request URI internally | rewrite /app/* /index.html |
basicauth | Protect routes with HTTP Basic Auth | basicauth /admin/* { ... } |
tls | Configure TLS settings manually | tls internal |
log | Configure access logging | log { output file /var/log/caddy/access.log } |
handle | Group directives for mutual exclusivity | handle /api/* { ... } |
handle_path | Like handle, but strips the matched prefix | handle_path /api/* { ... } |
respond | Return a static response | respond "OK" 200 |
import | Include another file or snippet | import /etc/caddy/snippets/* |
php_fastcgi | Proxy PHP requests to PHP-FPM | php_fastcgi unix//run/php/php-fpm.sock |
Reusable Snippets
Define reusable configuration blocks with snippets:
# Define a snippet
(security_headers) {
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"
-Server
}
}
# Use the snippet in site blocks
app.example.com {
import security_headers
reverse_proxy localhost:3000
}
api.example.com {
import security_headers
reverse_proxy localhost:8000
}
Troubleshooting
Certificate Issues
If Caddy fails to obtain a certificate, check these common causes:
# Verify DNS resolution
dig +short example.com
# Ensure ports 80 and 443 are reachable
sudo ss -tlnp | grep -E ':80|:443'
# Check if another service is using port 80
sudo lsof -i :80
# View Caddy logs for ACME errors
sudo journalctl -u caddy --no-pager | grep -i "acme\|certificate\|tls"
Common causes of certificate failures:
- DNS not pointing to your server — The domain must resolve to the server’s public IP
- Port 80 blocked by firewall — Required for the HTTP-01 challenge
- Another service using port 80 — Stop Nginx, Apache, or any other web server
- Rate limits — Let’s Encrypt limits certificate issuance to 5 per domain per week
502 Bad Gateway
This means Caddy cannot reach the upstream backend:
# Verify the backend is running
curl -I http://localhost:3000
# Check if the backend is bound to localhost or all interfaces
sudo ss -tlnp | grep 3000
# Common fix: ensure the backend is listening on 127.0.0.1, not 0.0.0.0
Permission Errors
# Ensure the caddy user has read access to the web root
sudo chown -R caddy:caddy /var/www/mysite
# Check file permissions
ls -la /var/www/mysite/
# If Caddy cannot bind to ports 80/443
sudo setcap 'cap_net_bind_service=+ep' /usr/bin/caddy
Configuration Validation
# Always validate before reloading
caddy validate --config /etc/caddy/Caddyfile
# Format the Caddyfile (fix indentation)
caddy fmt --overwrite /etc/caddy/Caddyfile
# Test with a specific adapter (e.g., for JSON config)
caddy adapt --config /etc/caddy/Caddyfile
Caddy Admin API
Caddy exposes a local admin API on localhost:2019 for runtime configuration:
# View current configuration as JSON
curl http://localhost:2019/config/
# Check loaded certificates
curl http://localhost:2019/pki/ca/local
# Reload configuration via API
curl -X POST http://localhost:2019/load \
-H "Content-Type: text/caddyfile" \
--data-binary @/etc/caddy/Caddyfile
Summary
Caddy eliminates the complexity of web server configuration by providing automatic HTTPS, a minimal Caddyfile syntax, and production-ready defaults. Whether you are serving static files, reverse-proxying to a backend application, or load balancing across multiple instances, Caddy handles the heavy lifting — including TLS certificate management — so you can focus on building your application.
For more on reverse proxy configuration with Nginx, see our Nginx Reverse Proxy Complete Guide. If you need to manage certificates manually with Certbot for servers that do not use Caddy, check out Automate SSL Certificates with Let’s Encrypt and Certbot.