CADDY AUTOMATIC HTTPS ARCHITECTURE Clients Browser / API Mobile / CLI HTTPS :443 Caddy Server Auto HTTPS (ACME) Reverse Proxy Load Balancing Compression HTTP/2 & HTTP/3 reverse_proxy HTTP Node.js :3000 app.example.com Python :8000 api.example.com Docker :9000 admin.example.com Let's Encrypt ACME CA Caddy obtains certificates automatically and proxies to your backends

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:

FeatureCaddyNginx
HTTPSAutomatic (built-in ACME client)Manual (requires Certbot or similar)
Configuration syntaxCaddyfile (minimal, human-readable)nginx.conf (powerful but verbose)
HTTP/2Enabled by defaultRequires explicit configuration
HTTP/3 (QUIC)Built-in, enabled by defaultExperimental (requires separate build)
Reverse proxyBuilt-in directiveBuilt-in module
Load balancingBuilt-in with multiple policiesBuilt-in (round-robin, least_conn, etc.)
Configuration reloadZero-downtime via API or SIGHUPZero-downtime via nginx -s reload
LanguageGo (memory-safe)C (high performance)
Memory usageLow (~20-50 MB)Very low (~5-15 MB)
Raw throughputVery goodExcellent (handles millions of RPS)
Community & ecosystemGrowing rapidlyMassive, 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:

  1. Obtain a TLS certificate for example.com automatically
  2. Serve files from /var/www/mysite
  3. Enable HTTP/2 and HTTP/3
  4. 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:

  1. Checks DNS — Verifies the domain resolves to the server’s public IP
  2. Requests a certificate — Contacts Let’s Encrypt (or ZeroSSL as fallback) via the ACME protocol
  3. Completes the HTTP-01 challenge — Proves domain ownership by serving a token on port 80
  4. Installs the certificate — Configures TLS with the obtained certificate and key
  5. Redirects HTTP to HTTPS — Creates an automatic redirect on port 80
  6. 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_ca directive 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:

PolicyDescription
randomChoose a random backend
least_connSend to the backend with fewest active connections
round_robinCycle through backends sequentially (default)
firstAlways use the first available backend
ip_hashRoute based on client IP for session affinity
uri_hashRoute based on request URI
headerRoute based on a request header value
cookieRoute 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 caddy user with sudo 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:

DirectivePurposeExample
reverse_proxyProxy requests to backend serversreverse_proxy localhost:3000
file_serverServe static files from diskfile_server
rootSet the document root directoryroot * /var/www/html
encodeEnable response compressionencode gzip zstd
headerSet, add, or remove response headersheader X-Frame-Options "DENY"
redirRedirect requests to a new URLredir /old /new permanent
rewriteRewrite the request URI internallyrewrite /app/* /index.html
basicauthProtect routes with HTTP Basic Authbasicauth /admin/* { ... }
tlsConfigure TLS settings manuallytls internal
logConfigure access logginglog { output file /var/log/caddy/access.log }
handleGroup directives for mutual exclusivityhandle /api/* { ... }
handle_pathLike handle, but strips the matched prefixhandle_path /api/* { ... }
respondReturn a static responserespond "OK" 200
importInclude another file or snippetimport /etc/caddy/snippets/*
php_fastcgiProxy PHP requests to PHP-FPMphp_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.