Caddy Web Server: The Easiest Way to Get Automatic HTTPS and Reverse Proxy

Caddy Web Server: The Easiest Way to Get Automatic HTTPS and Reverse Proxy


If you’ve ever set up Nginx or Apache and spent hours wrestling with SSL certificates, virtual host configs, and cryptic error messages, Caddy will feel like a breath of fresh air. Caddy is a modern, open-source web server written in Go that provides automatic HTTPS out of the box — no Certbot, no cron jobs, no manual certificate renewal. You just point it at your domain and it handles the rest.

In this guide, you’ll learn how to install Caddy, serve static sites, set up reverse proxies, configure multiple domains, and use the Caddyfile for production-ready deployments.

Why Caddy?

Caddy stands out from traditional web servers for several reasons:

  • Automatic HTTPS: Caddy obtains and renews TLS certificates from Let’s Encrypt (or ZeroSSL) automatically. No setup required.
  • Simple configuration: The Caddyfile syntax is human-readable and dramatically simpler than Nginx or Apache configs.
  • HTTP/2 and HTTP/3: Enabled by default with no extra configuration.
  • Single binary: Caddy ships as a single static binary — no dependencies, no package managers needed.
  • Reverse proxy built-in: Load balancing, health checks, header manipulation, and WebSocket proxying are all native.
  • Extensible with plugins: Add functionality like rate limiting, caching, or authentication via Caddy modules.

Caddy is used in production by companies and developers who want a web server that “just works” without the operational overhead of managing TLS manually.

Installing Caddy

On Ubuntu / Debian

Caddy provides official packages via their APT repository:

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

After installation, Caddy runs as a systemd service and listens on port 80 with a default welcome page.

On Fedora / RHEL / CentOS

dnf install 'dnf-command(copr)'
dnf copr enable @caddy/caddy
dnf install caddy

On macOS (Homebrew)

brew install caddy

Using Docker

docker run -d \
  --name caddy \
  -p 80:80 \
  -p 443:443 \
  -v caddy_data:/data \
  -v caddy_config:/config \
  -v $PWD/Caddyfile:/etc/caddy/Caddyfile \
  -v $PWD/site:/srv \
  caddy:latest

The /data volume is critical — it stores your TLS certificates. Don’t lose it.

Download the Binary Directly

You can also grab the binary from the official download page at https://caddyserver.com/download, where you can even add plugins before downloading.

# Verify installation
caddy version

Serving a Static Site

The simplest use case is serving static files. Create a Caddyfile in your project directory:

localhost {
    root * /var/www/mysite
    file_server
}

Then run:

caddy run

That’s it. Caddy is now serving files from /var/www/mysite on https://localhost with a self-signed certificate for local development.

Serving with a Real Domain

To serve a site on a real domain with automatic HTTPS, just replace localhost with your domain:

example.com {
    root * /var/www/mysite
    file_server
}

When Caddy starts, it will:

  1. Automatically obtain a TLS certificate from Let’s Encrypt
  2. Set up HTTP → HTTPS redirect
  3. Serve your site over HTTPS with HTTP/2
  4. Renew the certificate before it expires

Requirements: Your DNS A record must point to the server’s IP, and ports 80 and 443 must be open.

Reverse Proxy Configuration

One of Caddy’s most popular use cases is as a reverse proxy in front of application servers. Here’s how to proxy traffic to a backend running on port 3000:

app.example.com {
    reverse_proxy localhost:3000
}

That single line gives you:

  • Automatic HTTPS with Let’s Encrypt
  • HTTP/2 support
  • WebSocket proxying
  • Proper header forwarding (X-Forwarded-For, X-Forwarded-Proto, etc.)

Multiple Backend Services

You can proxy multiple domains to different backends in one Caddyfile:

app.example.com {
    reverse_proxy localhost:3000
}

api.example.com {
    reverse_proxy localhost:8080
}

grafana.example.com {
    reverse_proxy localhost:3001
}

Load Balancing

Caddy can distribute traffic across multiple backends:

app.example.com {
    reverse_proxy localhost:3001 localhost:3002 localhost:3003 {
        lb_policy round_robin
        health_uri /health
        health_interval 30s
    }
}

Available load balancing policies include round_robin, least_conn, random, first, ip_hash, uri_hash, and header.

Path-Based Routing

Route different URL paths to different services:

example.com {
    reverse_proxy /api/* localhost:8080
    reverse_proxy /ws/* localhost:9000
    root * /var/www/frontend
    file_server
}

The Caddyfile in Depth

Setting Headers

example.com {
    header {
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        -Server
    }
    reverse_proxy localhost:3000
}

The -Server directive removes the Server header from responses.

Compression

Enable gzip and zstd compression:

example.com {
    encode gzip zstd
    reverse_proxy localhost:3000
}

Logging

Configure structured access logs:

example.com {
    log {
        output file /var/log/caddy/access.log {
            roll_size 100mb
            roll_keep 5
            roll_keep_for 720h
        }
        format json
    }
    reverse_proxy localhost:3000
}

Basic Authentication

Protect a route with a username and password:

# Generate a hashed password
caddy hash-password
# Enter your password when prompted

Then use it in your Caddyfile:

admin.example.com {
    basicauth {
        admin $2a$14$YourHashedPasswordHere
    }
    reverse_proxy localhost:8080
}

Redirects

www.example.com {
    redir https://example.com{uri} permanent
}

Wildcard Certificates

Caddy supports wildcard certificates using the DNS challenge. You’ll need a DNS provider plugin:

*.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
    }
}

To use the Cloudflare DNS plugin, download Caddy with the module from https://caddyserver.com/download or build it with xcaddy:

xcaddy build --with github.com/caddy-dns/cloudflare

Running Caddy as a Systemd Service

If you installed Caddy from the official package, it’s already set up as a systemd service. The default Caddyfile location is /etc/caddy/Caddyfile.

# Start Caddy
sudo systemctl start caddy

# Enable on boot
sudo systemctl enable caddy

# Check status
sudo systemctl status caddy

# Reload config without downtime
sudo systemctl reload caddy

# View logs
journalctl -u caddy --no-pager -f

The reload command is key — Caddy performs graceful config reloads with zero downtime, so you never need to restart the service to apply changes.

Caddy vs. Nginx: A Quick Comparison

FeatureCaddyNginx
Automatic HTTPS✅ Built-in❌ Needs Certbot
Config syntaxSimple CaddyfileComplex nginx.conf
HTTP/2✅ Default✅ Manual enable
HTTP/3 (QUIC)✅ Default⚠️ Experimental
Reverse proxy✅ Built-in✅ Built-in
PerformanceExcellentExcellent
Memory usageHigher (Go runtime)Lower (C)
Plugin ecosystemGrowingMassive
Configuration reloadGraceful, zero-downtimeGraceful with -s reload

Nginx still wins for ultra-high-traffic scenarios and raw performance at scale, but Caddy is the better choice when you want simplicity, automatic TLS, and fewer moving parts.

Docker Compose with Caddy

Here’s a production-ready Docker Compose setup using Caddy as a reverse proxy for multiple services:

version: "3.8"

services:
  caddy:
    image: caddy:latest
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"  # HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config

  app:
    image: node:20-alpine
    working_dir: /app
    volumes:
      - ./app:/app
    command: node server.js
    expose:
      - "3000"

  api:
    image: python:3.12-slim
    working_dir: /api
    volumes:
      - ./api:/api
    command: python -m uvicorn main:app --host 0.0.0.0 --port 8000
    expose:
      - "8000"

volumes:
  caddy_data:
  caddy_config:

And the corresponding Caddyfile:

app.example.com {
    reverse_proxy app:3000
}

api.example.com {
    reverse_proxy api:8000
}

Notice how Docker’s internal DNS lets you use service names (app, api) instead of localhost.

Caddy API: Programmatic Configuration

Caddy also has a powerful REST API for dynamic configuration. Instead of a Caddyfile, you can push JSON config:

# Get current config
curl localhost:2019/config/

# Update config
curl localhost:2019/config/ \
  -H "Content-Type: application/json" \
  -d '{
    "apps": {
      "http": {
        "servers": {
          "myserver": {
            "listen": [":443"],
            "routes": [{
              "match": [{"host": ["example.com"]}],
              "handle": [{
                "handler": "reverse_proxy",
                "upstreams": [{"dial": "localhost:3000"}]
              }]
            }]
          }
        }
      }
    }
  }'

The admin API listens on localhost:2019 by default and is only accessible locally. This is useful for automation, service discovery, and dynamic routing.

Useful Caddy Commands

# Run with a specific Caddyfile
caddy run --config /path/to/Caddyfile

# Validate your Caddyfile for syntax errors
caddy validate --config /path/to/Caddyfile

# Format your Caddyfile (auto-indent and clean up)
caddy fmt --overwrite /path/to/Caddyfile

# Adapt Caddyfile to JSON (see what Caddy actually uses internally)
caddy adapt --config /path/to/Caddyfile

# Reload config without restarting
caddy reload --config /path/to/Caddyfile

# Run in the background
caddy start --config /path/to/Caddyfile
caddy stop

Tips and Best Practices

  1. Always persist the /data volume in Docker — it stores your TLS certificates. Losing it means re-issuing certs and potentially hitting Let’s Encrypt rate limits.

  2. Use caddy validate before reloading to catch config errors early.

  3. Use caddy fmt to keep your Caddyfile consistently formatted.

  4. Set up log rotation with the roll_size and roll_keep directives to prevent logs from filling your disk.

  5. Use environment variables in your Caddyfile with {env.VARIABLE_NAME} syntax for secrets like API tokens.

  6. Check the Caddy community at https://caddy.community/ for support and examples.

  7. Monitor certificate status — while Caddy handles renewal automatically, check journalctl -u caddy periodically to ensure renewals succeed.

Further Reading

Caddy is one of those tools that makes you wonder why web server configuration was ever so complicated. If you’re tired of managing Certbot cron jobs, debugging Nginx config blocks, and manually renewing certificates, give Caddy a try — you might never go back.