Caddy — Web Server with Automatic HTTPS
Practical guide to Caddy — automatic TLS certificates, Caddyfile syntax, reverse proxy, static file server and CLI, with examples for everyday use.
Caddy is a modern web server written in Go that takes the most tedious part of running a website off your hands: it provisions and renews TLS certificates through Let's Encrypt or ZeroSSL fully automatically, with no certbot and no cron jobs to babysit. HTTP/2 and HTTP/3 are on by default, and the readable Caddyfile syntax gets you from an empty file to a working configuration in just a few lines. Whether you need a static file server, a reverse proxy or a PHP backend, Caddy covers the common tasks with clear directives. This guide walks you through the CLI commands and Caddyfile building blocks you reach for every day.
Service Management (systemctl)
systemctl start caddy — Start the Caddy web server.
systemctl start caddysystemctl stop caddy — Stop the Caddy web server.
systemctl stop caddysystemctl restart caddy — Restart Caddy (briefly drops connections). Use reload for zero-downtime config changes.
systemctl restart caddysystemctl reload caddy — Reload Caddy configuration with zero downtime. Caddy re-reads /etc/caddy/Caddyfile.
systemctl reload caddysystemctl status caddy — Show Caddy service status, PID, and recent log output.
systemctl status caddysystemctl enable caddy — Enable Caddy to start automatically on system boot.
systemctl enable caddysystemctl disable caddy — Disable Caddy from starting automatically on boot.
systemctl disable caddycaddy CLI Commands
caddy run — Start Caddy in the foreground using Caddyfile in the current directory. Logs go to stdout.
caddy runcaddy run --config <file> — Start Caddy in the foreground with a specific config file.
caddy run --config /etc/caddy/Caddyfilecaddy start — Start Caddy as a background daemon.
caddy startcaddy start --config <file> — Start Caddy as a background daemon with a specific config file.
caddy start --config /etc/caddy/Caddyfilecaddy stop — Stop the running Caddy background daemon.
caddy stopcaddy reload — Reload the Caddy configuration with zero downtime. Applies changes from the Caddyfile.
caddy reloadcaddy reload --config <file> — Reload Caddy with a specific config file.
caddy reload --config /etc/caddy/Caddyfilecaddy validate --config <file> — Validate a Caddyfile for syntax and configuration errors without starting the server.
caddy validate --config /etc/caddy/Caddyfilecaddy fmt <file> — Format (pretty-print) a Caddyfile in place according to canonical style.
caddy fmt /etc/caddy/Caddyfilecaddy fmt --overwrite <file> — Format and overwrite a Caddyfile in place.
caddy fmt --overwrite /etc/caddy/Caddyfilecaddy version — Show the installed Caddy version.
caddy versioncaddy list-modules — List all available Caddy modules (directives, handlers, matchers, etc.).
caddy list-modulescaddy upgrade — Upgrade Caddy to the latest release in-place (replaces the binary).
caddy upgradecaddy adapt --config <file> — Convert a Caddyfile to Caddy's native JSON configuration format.
caddy adapt --config Caddyfilecaddy environ — Print the environment variables Caddy will use at runtime.
caddy environCaddyfile: Basic Structure
Minimal site block. Caddy automatically provisions a TLS certificate for the domain.
example.com {
respond "Hello, World!"
}Serve multiple domains from the same block (comma-separated).
example.com, www.example.com {
# shared config
}Global options block (must be the first block). Sets the ACME email, on-demand TLS, and other global settings.
{
email admin@example.com
on_demand_tls {
ask http://localhost:9000/check
}
}Define a reusable config snippet and import it in site blocks.
(common) {
encode gzip zstd
header X-Frame-Options DENY
}
example.com {
import common
}Listen on a specific port without HTTPS (no TLS when using plain port addresses).
:8080 {
respond "dev server"
}Use Caddy's built-in internal CA for a locally-trusted TLS certificate (no internet required).
localhost {
tls internal
respond "local dev"
}Static File Server
Serve static files from a directory. The * matches all requests.
example.com {
root * /var/www/html
file_server
}Serve static files with directory listing enabled.
example.com {
root * /var/www/html
file_server browse
}SPA-mode: try the exact path, fall back to index.html for client-side routing.
example.com {
root * /var/www/html
try_files {path} /index.html
file_server
}Hide specific files or directories from being served.
file_server {
hide .git
hide *.secret
}caddy file-server --root ./public --listen :8080 — Instantly serve a local directory from the command line without a Caddyfile.
caddy file-server --root ./dist --listen :3000Reverse Proxy
Proxy all requests for a domain to a local backend on port 3000.
example.com {
reverse_proxy localhost:3000
}Proxy only requests matching a path prefix to a specific backend.
example.com {
reverse_proxy /api/* localhost:4000
}Load-balance across multiple backends using round-robin by default.
example.com {
reverse_proxy backend1:8080 backend2:8080
}Load-balance using the least-connections policy.
reverse_proxy localhost:3000 {
lb_policy least_conn
}Pass original host and client IP headers to the upstream backend.
reverse_proxy localhost:3000 {
header_up Host {upstream_hostport}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
}Configure active health checks on the upstream backend.
reverse_proxy localhost:3000 {
health_uri /healthz
health_interval 10s
health_timeout 2s
}Proxy to an HTTPS upstream while skipping TLS verification (for internal self-signed certs).
reverse_proxy https://upstream.example.com {
transport http {
tls_insecure_skip_verify
}
}Redirects & Rewrites
Permanently redirect www to the apex domain, preserving the request URI.
www.example.com {
redir https://example.com{uri} permanent
}Redirect all HTTP traffic to HTTPS. Caddy normally handles this automatically.
http://example.com {
redir https://example.com{uri}
}redir /old-path /new-path permanent — Permanently redirect a specific path to a new location (301).
redir /blog /news permanentredir /old-path /new-path temporary — Temporarily redirect a path (302).
redir /sale /deals temporaryrewrite /app/* /index.html — Internally rewrite requests matching a path to another path (no browser redirect).
rewrite /app/* /index.htmlrewrite * /index.php{query} — Rewrite all requests to index.php, preserving the query string (useful for PHP apps).
rewrite * /index.php{query}TLS / HTTPS
Caddy provisions a Let's Encrypt certificate automatically when given a public domain. No extra config required.
example.com {
# TLS is automatic — no config needed
}tls admin@example.com — Explicitly set the ACME registration email for this site block (overrides global setting).
tls admin@example.comtls /path/to/cert.pem /path/to/key.pem — Use a custom (manually managed) TLS certificate and private key.
tls /etc/ssl/certs/example.crt /etc/ssl/private/example.keytls internal — Use Caddy's built-in local CA to issue a certificate. Trusted automatically on the local machine.
localhost { tls internal }Restrict TLS protocol versions and cipher suites.
tls {
protocols tls1.2 tls1.3
ciphers TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
}Use DNS-01 ACME challenge (required for wildcard certs or servers not publicly accessible).
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}caddy trust — Install Caddy's local root CA into the system and browser trust stores (for localhost TLS).
caddy trustcaddy untrust — Remove Caddy's local root CA from trust stores.
caddy untrustHeaders & Compression
encode gzip zstd — Enable gzip and zstd response compression. Caddy chooses the best format the client supports.
encode gzip zstdencode zstd gzip — Enable compression, preferring zstd over gzip.
encode zstd gzipheader X-Frame-Options DENY — Set a custom response header.
header X-Frame-Options DENYheader -Server — Remove a response header. The minus prefix deletes the header.
header -ServerSet multiple security headers and remove the Server header in one block.
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
-Server
}header Cache-Control "public, max-age=31536000, immutable" — Set long-term caching headers for static assets.
header Cache-Control "public, max-age=31536000"header Access-Control-Allow-Origin * — Allow all origins for CORS (Cross-Origin Resource Sharing).
header Access-Control-Allow-Origin *PHP & FastCGI
Serve a PHP application via PHP-FPM using a Unix socket. Combines FastCGI, try_files, and file_server.
example.com {
root * /var/www/html
php_fastcgi unix//run/php/php8.3-fpm.sock
file_server
}php_fastcgi 127.0.0.1:9000 — Connect to PHP-FPM via TCP instead of a Unix socket.
php_fastcgi 127.0.0.1:9000Complete WordPress setup with PHP-FPM, static files, and compression.
example.com {
root * /var/www/wordpress
php_fastcgi unix//run/php/php8.3-fpm.sock
file_server
encode gzip zstd
}Matchers & Routing
Route /api/* to a backend and everything else to the file server.
handle /api/* {
reverse_proxy localhost:3000
}
handle {
file_server
}Like handle, but strips the matched path prefix before passing to the inner block.
handle_path /static/* {
root * /var/www/static
file_server
}Define a named matcher with @name and reference it in a directive.
@api path /api/*
reverse_proxy @api localhost:3000Match requests by header value and return a 403 for bots.
@bot header User-Agent *bot*
respond @bot 403Combine matchers: apply basic auth only to /admin/* on a specific host.
@secure {
host example.com
path /admin/*
}
basicauth @secure {
admin JDJhJDE0JG...
}Use the 'not' matcher to redirect everyone except www to the www domain.
@notwww not host www.example.com
redir @notwww https://www.example.com{uri}Logging
log — Enable access logging with defaults (JSON format to stderr).
logWrite access logs to a file.
log {
output file /var/log/caddy/access.log
}Access log with automatic log rotation: max 100 MB per file, keep 5 files, max 30 days.
log {
output file /var/log/caddy/access.log {
roll_size 100mb
roll_keep 5
roll_keep_for 720h
}
}Use human-readable console format instead of JSON for log output.
log {
format console
}Log to stdout in JSON format with debug level (useful in containers).
log {
output stdout
format json
level DEBUG
}journalctl -u caddy -f — Follow Caddy's live log output via systemd journal.
journalctl -u caddy -fjournalctl -u caddy --since "1 hour ago" — Show Caddy logs from the last hour.
journalctl -u caddy --since "1 hour ago"Admin API
curl -s http://localhost:2019/config/ — Get the current Caddy configuration as JSON via the admin API (default listen: localhost:2019).
curl -s http://localhost:2019/config/ | jq '.'curl -X POST http://localhost:2019/load -H 'Content-Type: text/caddyfile' --data-binary @Caddyfile — Load a Caddyfile via the admin API (zero-downtime reload).
curl -X POST http://localhost:2019/load -H 'Content-Type: text/caddyfile' --data-binary @Caddyfilecurl -X DELETE http://localhost:2019/config/ — Clear the current Caddy configuration via the admin API.
curl -X DELETE http://localhost:2019/config/curl -s http://localhost:2019/reverse_proxy/upstreams — List all reverse proxy upstream backends and their health status.
curl -s http://localhost:2019/reverse_proxy/upstreams | jq '.'Disable the admin API entirely in the global options block (for production hardening).
{
admin off
}Change the admin API listen address in the global options block.
{
admin localhost:2020
}Practical Examples
Production-ready static site with compression and security headers.
example.com {
root * /var/www/html
encode gzip zstd
file_server
header -Server
header X-Frame-Options DENY
}Reverse proxy to a Node.js or other backend app with compression and hardened headers.
app.example.com {
encode gzip zstd
reverse_proxy localhost:3000
header -Server
}Standard PHP site with PHP-FPM, static file serving, and gzip.
example.com {
root * /var/www/html
encode gzip
php_fastcgi unix//run/php/php8.3-fpm.sock
file_server
}Wildcard certificate via DNS challenge, routing subdomains to different backends.
*.example.com {
tls {
dns cloudflare {env.CF_TOKEN}
}
@app host app.example.com
handle @app {
reverse_proxy localhost:3000
}
}caddy reverse-proxy --from :80 --to localhost:3000 — Instantly run a reverse proxy from the command line without a Caddyfile.
caddy reverse-proxy --from :443 --to localhost:3000Return a static 200 for a health check endpoint, proxy everything else.
example.com {
respond /healthz 200
reverse_proxy localhost:3000
} Conclusion
Caddy raises the bar for comfortable web hosting: what means manual certificate management and pages of configuration on traditional servers, Caddy handles with sensible defaults and automatic HTTPS. To keep a new setup running smoothly, follow one simple routine — validate every change with caddy validate and tidy it with caddy fmt before you reload. Automatic HTTPS also needs ports 80 and 443 open and the domain publicly reachable, otherwise Caddy cannot complete the ACME challenge.
Further Reading
- Caddy – official documentation – reference and manual
- Caddy on GitHub – source code and releases
- Caddy (web server) – Wikipedia – background and history