Caddy on a VPS: Automatic HTTPS, Simple Reverse Proxy, and Zero-Config Web Server

Caddy on a VPS: Automatic HTTPS, Simple Reverse Proxy, and Zero-Config Web Server

Caddy is a modern web server written in Go — its killer feature is automatic HTTPS: it obtains and renews Let’s Encrypt certificates automatically for any domain in its configuration, with no Certbot, no cron jobs, and no manual renewal. The Caddyfile configuration is radically simpler than Nginx — a reverse proxy configuration that takes 20 lines in Nginx takes 3 lines in Caddy. For VPS deployments prioritizing operational simplicity, Caddy is the compelling alternative.

Caddy vs Nginx: When to Choose Each

  • Choose Caddy: Simplicity priority, automatic HTTPS without external tooling, HTTP/3 out-of-the-box, JSON API for dynamic config, less configuration surface area
  • Choose Nginx: Maximum performance, advanced caching (FastCGI microcache), fine-grained tuning, ModSecurity WAF, team already knows Nginx, complex load balancing

Step 1: Install Caddy

<code"># Install from official apt repository
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
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 -y caddy

# Verify
caddy version
sudo systemctl status caddy

Step 2: Caddyfile Configuration

<code">sudo nano /etc/caddy/Caddyfile
<code"># Caddy AUTOMATICALLY:
# 1. Obtains Let's Encrypt certificate for the domain
# 2. Renews it before expiry
# 3. Configures HTTPS redirects
# 4. Sets secure TLS defaults (TLS 1.2+, modern ciphers)
# No Certbot needed!

# ── Reverse proxy to Node.js app ─────────────────────────
app.yourdomain.com {
    reverse_proxy localhost:3000
}

# ── WordPress with PHP-FPM ────────────────────────────────
yourdomain.com, www.yourdomain.com {
    root * /var/www/wordpress
    php_fastcgi unix//run/php/php8.2-fpm.sock
    file_server
    encode gzip zstd   # Brotli not yet in stable Caddy
}

# ── Static file server ────────────────────────────────────
static.yourdomain.com {
    root * /var/www/static
    file_server
    encode gzip zstd
    header Cache-Control "public, max-age=31536000, immutable"
}

# ── Multiple reverse proxies ──────────────────────────────
api.yourdomain.com {
    reverse_proxy localhost:8000
}

grafana.yourdomain.com {
    reverse_proxy localhost:3001
}

minio.yourdomain.com {
    reverse_proxy localhost:9000 {
        # Large file uploads
        request_body {
            max_size 50GB
        }
    }
}

# ── Load balancing ────────────────────────────────────────
lb.yourdomain.com {
    reverse_proxy {
        to localhost:3001 localhost:3002 localhost:3003
        lb_policy round_robin
        health_uri /health
        health_interval 10s
    }
}

# ── Basic authentication ──────────────────────────────────
private.yourdomain.com {
    basic_auth {
        admin JDJhJDE0JHpLWE9ZY3...   # htpasswd hash
    }
    reverse_proxy localhost:8080
}

# ── Rate limiting (via order) ─────────────────────────────
login.yourdomain.com {
    rate_limit {
        zone login_zone {
            key    {remote_host}
            events 5
            window 1m
        }
    }
    reverse_proxy localhost:4000
}
<code"># Reload configuration (no downtime):
sudo caddy reload --config /etc/caddy/Caddyfile

# Validate configuration:
sudo caddy validate --config /etc/caddy/Caddyfile

# View active configuration:
sudo caddy environ

Step 3: Automatic HTTPS — How It Works

<code"># Caddy performs automatic HTTPS via:
# 1. DNS lookup: verify domain resolves to this server's IP
# 2. ACME HTTP-01 challenge: Let's Encrypt verifies domain ownership
# 3. Certificate issued and stored in /var/lib/caddy/.local/share/caddy
# 4. Renewal: ~30 days before expiry, Caddy renews automatically
# 5. OCSP stapling: includes certificate validity in TLS handshake

# Check certificate status:
sudo caddy certificates

# View certificate directory:
sudo ls /var/lib/caddy/.local/share/caddy/certificates/

Step 4: HTTP/3 and Performance

<code"># Caddy enables HTTP/3 (QUIC) by default for supported clients
# Verify HTTP/3 is active:
curl -I --http3 https://yourdomain.com 2>&1 | head -5
# Should show: HTTP/3 200

# Open firewall for UDP 443 (required for HTTP/3/QUIC):
sudo ufw allow 443/udp

# Performance settings:
yourdomain.com {
    reverse_proxy localhost:3000
    
    # Compression
    encode gzip zstd
    
    # Security headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy strict-origin-when-cross-origin
    }
    
    # Response caching headers for static assets
    @static {
        path *.css *.js *.png *.jpg *.ico *.woff2
    }
    header @static Cache-Control "public, max-age=31536000, immutable"
}

Step 5: Dynamic Configuration via API

<code"># Caddy has a REST API for dynamic configuration (useful for Docker deployments)
# Enable admin API:
{
    admin localhost:2019
}

# Add a route dynamically:
curl -X POST http://localhost:2019/load \
  -H "Content-Type: application/json" \
  -d '{
    "apps": {
      "http": {
        "servers": {
          "srv0": {
            "listen": [":443"],
            "routes": [{
              "match": [{"host": ["newapp.yourdomain.com"]}],
              "handle": [{
                "handler": "reverse_proxy",
                "upstreams": [{"dial": "localhost:9000"}]
              }]
            }]
          }
        }
      }
    }
  }'

Step 6: Migrate from Nginx

<code"># Common Nginx → Caddy migrations:

# Nginx:
# server {
#     listen 80;
#     server_name example.com;
#     return 301 https://$host$request_uri;
# }
# server {
#     listen 443 ssl;
#     server_name example.com;
#     ssl_certificate /etc/letsencrypt/.../fullchain.pem;
#     ssl_certificate_key /etc/letsencrypt/.../privkey.pem;
#     location / {
#         proxy_pass http://127.0.0.1:3000;
#         proxy_set_header Host $host;
#         proxy_set_header X-Real-IP $remote_addr;
#         proxy_set_header X-Forwarded-Proto $scheme;
#     }
# }

# Caddy equivalent (4 lines vs 20+):
# example.com {
#     reverse_proxy localhost:3000
# }
# (HTTPS redirect, SSL cert, headers all automatic)

Getting Started

Caddy uses 20–50 MB RAM — lighter than Nginx for typical configurations. Install on any Ubuntu VPS at VPS.DO and add your first site to the Caddyfile — certificate provisioning happens automatically on first request to the domain. For VPS deployments running multiple apps, Caddy’s one-line reverse proxy configuration and automatic certificate management eliminate 80% of Nginx’s operational overhead.

Conclusion

Caddy’s automatic HTTPS is genuinely transformative for VPS operations — no Certbot, no cron jobs, no renewal failures. The Caddyfile syntax is orders of magnitude simpler than Nginx for reverse proxy use cases. HTTP/3 support, live config reloads, and the JSON API for dynamic configuration make Caddy a compelling choice for modern VPS deployments where operational simplicity matters as much as raw performance. For teams spending hours on Nginx SSL configuration and renewal management, Caddy reclaims that time immediately.

Post Your Comment

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!