PQ
PQ.Hosting

Currency

Caddy: The Web Server That Handles SSL Automatically Without Config Files

Author
PQ
March 25, 2026
5 min read
24 views
Caddy: The Web Server That Handles SSL Automatically Without Config Files

With Nginx you need to install certbot, write a virtual host config, set up a cronjob for certificate renewal, and come back in 90 days to verify everything still works. With Caddy that entire list is replaced by one line. Not because Caddy is smarter — but because HTTPS is built into its architecture, not bolted on top.

Installation

Debian / Ubuntu — official 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

Check version:

caddy version

Caddy automatically creates a systemd unit when installed from the package. Check status:

systemctl status caddy

How Automatic HTTPS Works

Caddy uses the ACME protocol (the same one Let's Encrypt uses) directly — without certbot as a middleman. On first run with a domain name Caddy:

  1. Generates a private key
  2. Completes the ACME challenge (HTTP-01 or TLS-ALPN-01)
  3. Obtains a certificate from Let's Encrypt or ZeroSSL
  4. Stores it in ~/.local/share/caddy/ (or /var/lib/caddy/)
  5. Automatically renews 30 days before expiry

No cron, no certbot renew. The certificate maintains itself.

Important for VPS: port 80 must be open for the HTTP-01 challenge, and DNS must point to the server before running Caddy with a domain for the first time.

Caddyfile: The Configuration Language

Caddyfile is a declarative format. A block starts with an address, directives go inside.

Simplest static site with automatic HTTPS:

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

That is it. Caddy gets the certificate, sets up HTTP to HTTPS redirect, and serves files.

Multiple domains in one file:

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

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

Apply a new config without restart (graceful reload):

sudo systemctl reload caddy

Or directly:

caddy reload --config /etc/caddy/Caddyfile

Reverse Proxy: The Most Common VPS Scenario

App on Node.js / Python / Go listening on localhost:3000 — Caddy proxies it externally with HTTPS:

api.example.com {
    reverse_proxy localhost:3000
}

Two lines. Caddy adds X-Forwarded-For and X-Real-IP headers automatically.

Load balancing across multiple backends:

app.example.com {
    reverse_proxy localhost:3000 localhost:3001 localhost:3002
}

Default policy is round-robin. Add health checks:

app.example.com {
    reverse_proxy localhost:3000 localhost:3001 {
        health_uri /health
        health_interval 10s
        health_timeout 2s
    }
}

Multiple Services on One Server

Classic VPS scenario — several projects on one IP. With Nginx you need a separate config and certificate for each. With Caddy:

site1.example.com {
    reverse_proxy localhost:8001
}

site2.example.com {
    reverse_proxy localhost:8002
}

api.example.com {
    reverse_proxy localhost:4000
    header {
        Access-Control-Allow-Origin *
    }
}

Each domain gets its own certificate automatically. Caddy requests them in parallel at startup.

Basic Authentication

Protect a private service with a password.

Generate a password hash:

caddy hash-password --plaintext "mypassword"

Add to config:

private.example.com {
    basicauth {
        admin JDJhJDE0JHBxZ3...hash...
    }
    reverse_proxy localhost:8080
}

Caddy in Docker

docker-compose.yml for Caddy with automatic HTTPS:

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

  app:
    image: myapp:latest
    expose:
      - "3000"

volumes:
  caddy_data:
  caddy_config:

Caddyfile alongside:

example.com {
    reverse_proxy app:3000
}

caddy_data is the volume for certificates. It is critical to persist this between container restarts — otherwise Caddy requests a new certificate on every start and will hit Let's Encrypt's rate limit (50 certificates per domain per week).

Local Development: HTTPS Without Let's Encrypt

For localhost and internal domains Caddy creates its own local CA and issues certificates from it:

caddy run --config Caddyfile
localhost {
    reverse_proxy localhost:3000
}

Add the local CA to trusted roots once:

caddy trust

After this the browser accepts https://localhost without warnings. This works through a mechanism similar to mkcert built directly into Caddy.

Comparison With Nginx for Typical VPS Tasks

Task Nginx Caddy
Static site with HTTPS config + certbot + cron 2 lines in Caddyfile
Reverse proxy 10+ config lines 3 lines
Certificate renewal manual or cron automatic
Multiple domains separate file for each one Caddyfile
HTTP/2 enable manually on by default
HTTP/3 (QUIC) experimental on by default
Config via API no built-in REST API
Memory usage ~5–10 MB ~20–30 MB

Nginx is faster under very high load and has more modules. Caddy wins when you need to spin up a service quickly without operational overhead of managing certificates.

Management API: Change Config Without Files

Caddy has a built-in REST API on localhost:2019. Change configuration without editing files:

View current configuration:

curl localhost:2019/config/

Add a new route on the fly:

curl -X POST localhost:2019/load \
  -H "Content-Type: application/json" \
  -d @new-config.json

Useful in automation — for example deploy scripts that add new subdomains without reloading the server.

Quick Reference

Task Command / config
Static site with HTTPS example.com { root * /path; file_server }
Reverse proxy example.com { reverse_proxy localhost:3000 }
Reload config sudo systemctl reload caddy
Validate config caddy validate --config /etc/caddy/Caddyfile
Hash a password caddy hash-password --plaintext "pass"
Trust local CA caddy trust
Current config via API curl localhost:2019/config/
Where certificates are stored /var/lib/caddy/.local/share/caddy/

 

Share this article