Caddy SSL/TLS Configuration
Caddy is a modern web server with automatic HTTPS by default. It obtains and renews TLS certificates from Let's Encrypt and ZeroSSL without any configuration, making it one of the simplest ways to serve secure websites.
Automatic HTTPS (Default Behavior)
Basic site with automatic TLS
Caddy automatically obtains and renews certificates when you use a qualifying domain name. No TLS configuration is needed:
example.com {
root * /var/www/html
file_server
}Caddy will automatically obtain a certificate from Let's Encrypt or ZeroSSL, serve the site over HTTPS on port 443, and redirect HTTP to HTTPS on port 80.
Multi-domain automatic TLS
Serve multiple domains with automatic certificates for each:
example.com {
root * /var/www/example
file_server
}
www.example.com {
redir https://example.com{uri}
}
api.example.com {
reverse_proxy localhost:8080
}Each site block with a qualifying domain automatically gets its own certificate. Caddy batches requests to the CA when possible.
Multiple domains in one site block
A single site can handle multiple domain names with one SAN certificate:
example.com, www.example.com {
root * /var/www/html
file_server
}Wildcard certificates with DNS challenge
Wildcard certificates require the DNS challenge. You must configure a DNS provider module:
*.example.com {
tls {
dns cloudflare {env.CF_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 "Not Found" 404
}
}Caddy must be built with the appropriate DNS provider plugin (e.g., caddy-dns/cloudflare). Use xcaddy build --with github.com/caddy-dns/cloudflare to build with the plugin.
Automatic HTTPS redirect behavior
Caddy automatically creates HTTP-to-HTTPS redirects. You can customize this behavior:
# Automatic HTTPS is enabled by default for qualifying domains
# To disable automatic HTTPS entirely:
{
auto_https off
}
# To disable only HTTP-to-HTTPS redirects (keep auto certs):
{
auto_https disable_redirects
}
# To disable certificate management only (keep redirects):
{
auto_https disable_certs
}HTTP-only site (no TLS)
Use a protocol prefix or port to serve HTTP without automatic HTTPS:
# Serve on HTTP only using explicit protocol
http://example.com {
root * /var/www/html
file_server
}
# Or serve on a specific port (no auto HTTPS)
:8080 {
respond "Hello, HTTP!"
}TLS Directive Configuration
Specify certificate and key files
Use your own certificate and private key instead of automatic ACME:
example.com {
tls /etc/ssl/certs/example.com.pem /etc/ssl/private/example.com.key
reverse_proxy localhost:8080
}The certificate file should include the full chain (leaf + intermediates). Providing your own certificate disables automatic ACME issuance for that site.
Specify ACME email address
Set the email address used for ACME account registration:
example.com {
tls [email protected]
reverse_proxy localhost:8080
}
# Or set it globally for all sites:
{
email [email protected]
}
example.com {
reverse_proxy localhost:8080
}Internal (self-signed) certificates
Use Caddy's internal CA for local development or internal services:
# Use Caddy's built-in CA for self-signed certificates
localhost {
tls internal
respond "Hello from local HTTPS!"
}
# Internal TLS with specific options
intranet.local {
tls internal {
on_demand
}
reverse_proxy localhost:3000
}Run caddy trust to install Caddy's root CA certificate into your system trust store so browsers accept these certificates.
TLS protocol versions
Restrict the minimum and maximum TLS protocol versions:
example.com {
tls {
protocols tls1.2 tls1.3
}
reverse_proxy localhost:8080
}
# TLS 1.3 only (maximum security)
secure.example.com {
tls {
protocols tls1.3
}
reverse_proxy localhost:9090
}The first argument is the minimum version, the second is the maximum. Supported values: tls1.2 and tls1.3. Caddy does not support TLS 1.0 or 1.1.
Cipher suite configuration
Specify allowed cipher suites for TLS 1.2 connections (TLS 1.3 cipher suites are not configurable):
example.com {
tls {
ciphers TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
}
reverse_proxy localhost:8080
}Caddy uses Go's standard TLS cipher suite names. Only TLS 1.2 cipher suites can be configured; TLS 1.3 suites are always enabled and cannot be changed.
Curve preferences
Configure the elliptic curves used for key exchange:
example.com {
tls {
curves x25519 secp256r1 secp384r1
}
reverse_proxy localhost:8080
}Supported curves: x25519, secp256r1, secp384r1, secp521r1. The order specifies preference.
Certificate key type
Specify the type of key to use when generating CSRs for ACME:
example.com {
tls {
key_type p256
}
reverse_proxy localhost:8080
}Supported key types: ed25519, p256, p384, rsa2048, rsa4096.
ACME Configuration
Custom ACME CA
Use a different ACME Certificate Authority instead of the defaults:
# Per-site ACME CA
example.com {
tls {
issuer acme {
dir https://acme.example.com/directory
}
}
reverse_proxy localhost:8080
}
# Global ACME CA for all sites
{
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}Use the Let's Encrypt staging URL during testing to avoid hitting production rate limits.
DNS challenge provider
Configure the DNS challenge for certificates (required for wildcard certs and useful when ports 80/443 are unavailable):
# Cloudflare DNS challenge
example.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
reverse_proxy localhost:8080
}
# Route53 DNS challenge
example.com {
tls {
dns route53 {
region us-east-1
access_key_id {env.AWS_ACCESS_KEY_ID}
secret_access_key {env.AWS_SECRET_ACCESS_KEY}
}
}
reverse_proxy localhost:8080
}
# DNS challenge with propagation settings
example.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
resolvers 1.1.1.1
propagation_timeout 120s
}
reverse_proxy localhost:8080
}DNS provider modules must be compiled into Caddy. Build a custom binary using xcaddy with the required plugin.
ACME issuer with full options
Fine-grained control over the ACME issuer configuration:
example.com {
tls {
issuer acme {
dir https://acme-v02.api.letsencrypt.org/directory
email [email protected]
timeout 30s
disable_http_challenge
disable_tlsalpn_challenge
alt_http_port 5080
alt_tlsalpn_port 5443
preferred_chains {
root_common_name "ISRG Root X1"
}
}
}
reverse_proxy localhost:8080
}ZeroSSL issuer
Explicitly use ZeroSSL as the certificate authority:
example.com {
tls {
issuer zerossl {env.ZEROSSL_API_KEY}
}
reverse_proxy localhost:8080
}On-demand TLS
Obtain certificates at the time of the first TLS handshake, rather than at config load. Useful for serving many domains dynamically:
# Global on-demand TLS settings (required)
{
on_demand_tls {
ask http://localhost:5555/check
interval 2m
burst 5
}
}
# Site using on-demand TLS
https:// {
tls {
on_demand
}
reverse_proxy localhost:8080
}The ask endpoint must return HTTP 200 to allow certificate issuance for a domain. This prevents abuse. The interval and burst settings rate-limit on-demand issuance.
Client Certificate Authentication (mTLS)
Require client certificates
Configure mutual TLS by requiring clients to present a valid certificate:
example.com {
tls {
client_auth {
mode require_and_verify
trusted_ca_cert_file /etc/ssl/client-ca.pem
}
}
reverse_proxy localhost:8080
}Client authentication modes
Caddy supports multiple client certificate verification modes:
# Require and verify (strictest - default when client_auth is used)
tls {
client_auth {
mode require_and_verify
trusted_ca_cert_file /etc/ssl/client-ca.pem
}
}
# Request but don't require (optional client certs)
tls {
client_auth {
mode request
}
}
# Verify if given (optional, but verify if presented)
tls {
client_auth {
mode verify_if_given
trusted_ca_cert_file /etc/ssl/client-ca.pem
}
}
# Require any certificate (no verification against CA)
tls {
client_auth {
mode require
}
}Multiple trusted CA certificates
Trust multiple CA certificates for client authentication:
example.com {
tls {
client_auth {
mode require_and_verify
trusted_ca_cert_file /etc/ssl/ca1.pem
trusted_ca_cert_file /etc/ssl/ca2.pem
}
}
reverse_proxy localhost:8080
}Leaf certificate matching
Trust specific leaf (client) certificates directly instead of a CA:
example.com {
tls {
client_auth {
mode require_and_verify
trusted_leaf_cert_file /etc/ssl/allowed-client.pem
}
}
reverse_proxy localhost:8080
}Leaf certificate matching pins to specific client certificates. This is useful when you have a small, known set of clients and do not need a full CA infrastructure.
Certificate Management
Certificate storage location
Caddy stores certificates and ACME account data in a data directory. You can customize the storage backend:
# Default storage location:
# Linux: $HOME/.local/share/caddy (or $XDG_DATA_HOME/caddy)
# macOS: $HOME/Library/Application Support/Caddy
# Custom file system storage
{
storage file_system {
root /var/lib/caddy/certificates
}
}Multiple certificate issuers
Configure fallback issuers in case the primary CA is unavailable:
example.com {
tls {
issuer acme {
dir https://acme-v02.api.letsencrypt.org/directory
email [email protected]
}
issuer zerossl {env.ZEROSSL_API_KEY}
}
reverse_proxy localhost:8080
}Caddy will try issuers in the order they are defined. If the first issuer fails, it falls back to the next one.
Certificate automate via global options
Manage certificates for domains that are not served directly by Caddy:
{
cert_issuer acme
}
# Caddy will obtain and renew certificates even for domains
# only referenced in reverse_proxy or other directivesLoading certificate and key files
Load certificates from files, including dual RSA and ECDSA certificates:
# Provide cert and key files directly
example.com {
tls /etc/ssl/certs/example.com.pem /etc/ssl/private/example.com.key
}OCSP Stapling
Default OCSP stapling behavior
Caddy enables OCSP stapling by default for all managed certificates:
# OCSP stapling is automatic - no configuration needed.
# Caddy will:
# 1. Fetch OCSP responses for all managed certificates
# 2. Cache OCSP responses and refresh before expiry
# 3. Staple OCSP responses to TLS handshakes
# 4. Replace revoked certificates automatically
example.com {
reverse_proxy localhost:8080
}Caddy checks OCSP status regularly. If a certificate is found to be revoked, Caddy will attempt to replace it automatically.
OCSP stapling with custom certificates
OCSP stapling also works with manually loaded certificates, as long as the OCSP responder URL is included in the certificate:
# Custom certs get OCSP stapling too, if the certificate
# contains an OCSP responder URL in its AIA extension
example.com {
tls /etc/ssl/certs/example.com.pem /etc/ssl/private/example.com.key
reverse_proxy localhost:8080
}Verify OCSP stapling
Test that OCSP stapling is working correctly:
# Check OCSP stapling with OpenSSL
echo QUIT | openssl s_client -connect example.com:443 -status 2>&1 | grep -A 17 "OCSP response:"
# Expected output includes:
# OCSP Response Status: successful (0x0)
# Cert Status: goodSecurity Headers
HSTS (HTTP Strict Transport Security)
Add HSTS headers to instruct browsers to always use HTTPS:
example.com {
header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
reverse_proxy localhost:8080
}Note: Caddy already provides HTTPS by default. HSTS adds an extra layer by telling browsers to never attempt an unencrypted connection.
Comprehensive security headers
Add a full set of security headers for defense in depth:
example.com {
header {
# HSTS (2 years)
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
# Prevent MIME type sniffing
X-Content-Type-Options "nosniff"
# Clickjacking protection
X-Frame-Options "DENY"
# Referrer policy
Referrer-Policy "strict-origin-when-cross-origin"
# Content Security Policy
Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
# Permissions Policy
Permissions-Policy "geolocation=(), microphone=(), camera=()"
# Remove server identifier
-Server
}
reverse_proxy localhost:8080
}The -Server prefix removes the Server header from responses. Caddy's header directive can set, add, or delete response headers.
Security headers as a reusable snippet
Define security headers once and reuse them across multiple sites:
(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"
Permissions-Policy "geolocation=(), microphone=(), camera=()"
-Server
}
}
example.com {
import security_headers
reverse_proxy localhost:8080
}
api.example.com {
import security_headers
reverse_proxy localhost:9090
}Snippets are defined with parentheses and imported with the import directive. They are a powerful way to keep Caddyfiles DRY.
Reverse Proxy with TLS
Reverse proxy to HTTPS backend
Forward requests to an upstream that uses HTTPS:
example.com {
reverse_proxy https://backend.internal:8443
}When the upstream URL uses https://, Caddy automatically connects to it over TLS and verifies its certificate.
Skip backend certificate verification
Connect to an HTTPS backend with a self-signed or untrusted certificate:
example.com {
reverse_proxy https://backend.internal:8443 {
transport http {
tls_insecure_skip_verify
}
}
}Use this only for development or when the backend certificate cannot be properly verified. In production, prefer configuring a trusted CA.
Custom CA for backend verification
Specify a custom root CA to verify the upstream's certificate:
example.com {
reverse_proxy https://backend.internal:8443 {
transport http {
tls_trusted_ca_certs /etc/ssl/internal-ca.pem
}
}
}Client certificate to backend (mTLS upstream)
Present a client certificate when connecting to the upstream:
example.com {
reverse_proxy https://backend.internal:8443 {
transport http {
tls_client_auth /etc/ssl/client.pem /etc/ssl/client-key.pem
tls_trusted_ca_certs /etc/ssl/internal-ca.pem
}
}
}TLS server name override for backend
Override the server name used for TLS verification when the backend hostname differs from its certificate:
example.com {
reverse_proxy https://10.0.0.5:8443 {
transport http {
tls_server_name backend.example.com
}
}
}Complete Caddyfile Examples
Production Caddyfile with hardened TLS
A complete production-ready Caddyfile with TLS best practices:
# Global options
{
email [email protected]
}
# Security headers 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"
Permissions-Policy "geolocation=(), microphone=(), camera=()"
-Server
}
}
# Main site
example.com {
tls {
protocols tls1.2 tls1.3
curves x25519 secp256r1 secp384r1
}
import security_headers
encode gzip zstd
root * /var/www/example.com
file_server
log {
output file /var/log/caddy/example.com.log
}
}
# WWW redirect
www.example.com {
redir https://example.com{uri}
}
# API subdomain
api.example.com {
tls {
protocols tls1.2 tls1.3
}
import security_headers
reverse_proxy localhost:8080
log {
output file /var/log/caddy/api.example.com.log
}
}Multi-site configuration
Serve multiple sites with different configurations, each with automatic certificates:
{
email [email protected]
}
# Marketing site - static files
marketing.company.com {
root * /var/www/marketing
file_server
encode gzip
}
# Application - reverse proxy
app.company.com {
reverse_proxy localhost:3000
}
# API - reverse proxy with CORS
api.company.com {
@cors_preflight method OPTIONS
handle @cors_preflight {
header Access-Control-Allow-Origin "https://app.company.com"
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization"
respond "" 204
}
header Access-Control-Allow-Origin "https://app.company.com"
reverse_proxy localhost:8080
}
# Admin panel - IP restricted
admin.company.com {
@blocked not remote_ip 10.0.0.0/8
respond @blocked "Forbidden" 403
reverse_proxy localhost:9090
}Wildcard site with DNS challenge
Full wildcard setup with DNS-based certificate validation:
*.example.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
@blog host blog.example.com
handle @blog {
reverse_proxy localhost:2368
}
@shop host shop.example.com
handle @shop {
reverse_proxy localhost:3000
}
@docs host docs.example.com
handle @docs {
root * /var/www/docs
file_server
}
# Default: not found
handle {
respond "Subdomain not configured" 404
}
}mTLS configuration
Complete mutual TLS setup for an internal service:
internal-api.example.com {
tls {
protocols tls1.3
client_auth {
mode require_and_verify
trusted_ca_cert_file /etc/ssl/internal-ca.pem
}
}
# Log client certificate info
log {
output file /var/log/caddy/mtls-access.log
format json
}
reverse_proxy localhost:8080
}Local development setup
Development configuration using Caddy's internal CA for local HTTPS:
# First run: caddy trust (installs Caddy's root CA)
localhost {
tls internal
# Frontend
handle /api/* {
reverse_proxy localhost:8080
}
handle {
reverse_proxy localhost:3000
}
}
# Alternative: multiple local services
https://app.localhost {
tls internal
reverse_proxy localhost:3000
}
https://api.localhost {
tls internal
reverse_proxy localhost:8080
}CLI Commands
Install CA certificate into system trust store
# Install Caddy's root CA certificate into your system trust store
# (requires admin/root privileges)
caddy trustRequired for browsers to trust certificates issued by Caddy's internal CA (used with tls internal).
Remove CA certificate from trust store
# Remove Caddy's root CA certificate from your system trust store
caddy untrust
# Untrust a specific CA certificate by file
caddy untrust --cert /path/to/ca-cert.pemList managed certificates
# List all managed certificates (requires running Caddy instance)
caddy certificatesValidate Caddyfile syntax
# Convert Caddyfile to JSON and validate
caddy adapt --config /etc/caddy/Caddyfile
# Validate with pretty output
caddy adapt --config /etc/caddy/Caddyfile --pretty
# Validate and check for warnings
caddy validate --config /etc/caddy/CaddyfileAlways validate your Caddyfile before reloading. The adapt command converts the Caddyfile to Caddy's native JSON format, and validate checks the entire configuration.
Reload configuration
# Reload Caddy configuration gracefully (zero downtime)
caddy reload --config /etc/caddy/Caddyfile
# Reload with a specific adapter
caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
# Or send a SIGUSR1 signal
kill -USR1 $(pidof caddy)Reloading is graceful: existing connections continue to be served while new connections use the updated configuration.
Run Caddy
# Start Caddy with a Caddyfile
caddy run --config /etc/caddy/Caddyfile
# Start in the background (daemon mode)
caddy start --config /etc/caddy/Caddyfile
# Stop a background Caddy instance
caddy stop
# Start with environment file
caddy run --config /etc/caddy/Caddyfile --envfile /etc/caddy/.envBuild Caddy with plugins (xcaddy)
# Install xcaddy
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
# Build with DNS provider plugin
xcaddy build --with github.com/caddy-dns/cloudflare
# Build with multiple plugins
xcaddy build \
--with github.com/caddy-dns/cloudflare \
--with github.com/caddy-dns/route53 \
--with github.com/greenpau/caddy-securityDNS challenge providers, storage backends, and other extensions are compiled into the Caddy binary. Use xcaddy to build a custom Caddy with the plugins you need.
Testing and Validation
Verify TLS configuration
Test TLS settings with common tools:
# Check TLS certificate details
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>&1 | openssl x509 -noout -text
# Verify TLS 1.3 support
openssl s_client -connect example.com:443 -tls1_3 < /dev/null
# Check supported cipher suites
nmap --script ssl-enum-ciphers -p 443 example.com
# Quick certificate check with curl
curl -vI https://example.com 2>&1 | grep -E "subject|expire|issuer"View Caddy logs for TLS issues
Debug certificate issuance and TLS handshake problems:
# Enable debug logging globally
{
debug
}
# Enable debug logging per site
example.com {
log {
output file /var/log/caddy/debug.log
level DEBUG
}
reverse_proxy localhost:8080
}
# View systemd logs for Caddy
journalctl -u caddy --no-pager -fSSL Labs and external tests
Use external services to validate your TLS configuration:
# SSL Labs: https://www.ssllabs.com/ssltest/
# Target: A+ rating (achievable with Caddy's defaults + HSTS)
# testssl.sh for local testing
git clone --depth 1 https://github.com/drwetter/testssl.sh.git
./testssl.sh/testssl.sh https://example.com
# Security Headers: https://securityheaders.com/Important Notes
For automatic HTTPS to work, your domain must resolve to your server, and ports 80 and 443 must be accessible. If behind a firewall or NAT, use the DNS challenge instead.
Let's Encrypt has rate limits (50 certificates per registered domain per week). Use the staging CA during development and testing to avoid hitting limits.
Wildcard certificates always require the DNS challenge. You must build Caddy with a DNS provider plugin using xcaddy. HTTP and TLS-ALPN challenges cannot validate wildcard domains.
Always configure the ask endpoint when using on-demand TLS to prevent abuse. Without it, anyone could trigger certificate issuance for arbitrary domains pointing to your server.
Caddy v2 has a completely different configuration format from v1. All examples on this page use Caddy v2 syntax. If upgrading from v1, consult the official migration guide.
Caddy's data directory (where certificates and keys are stored) should be accessible only to the Caddy process. Default permissions are set correctly when using the official packages.
Caddy automatically renews certificates before they expire (at about 2/3 of their lifetime). No cron jobs or manual intervention needed. Renewals are attempted well in advance and retried on failure.
Official Caddy documentation: caddyserver.com/docs. Caddyfile reference: caddyserver.com/docs/caddyfile. Community wiki and forums: caddy.community.