FREE GUIDE Plan 1c · Tested 2026-07-22

Set Up Free HTTPS
with Caddy

5 minutes · 4 steps · Automatic Let's Encrypt certificates for any backend

Want the full version? Multi-domain setup, load balancing, compression tuning, and HTTP/3 configuration.

Get the Full Pack →

What You'll End Up With

Why Caddy Instead of Nginx

FeatureCaddyNginx + Certbot
HTTPS setupZero config30+ line config + certbot cron
Certificate renewalAutomatic, in-processCron job that sometimes fails
Config languageHuman-readableDSL with 200+ directives
HTTP/3Built-inRequires compile from source
CompressionAuto, smartManual gzip config
Time to working HTTPS2 minutes30-60 minutes
If you're already running Nginx and it's working fine, keep it. This guide is for new setups or cases where you want zero-maintenance HTTPS. Don't switch just to switch.

Prerequisites

DNS must resolve first. Caddy validates domain ownership via Let's Encrypt's HTTP-01 challenge. If your domain doesn't point to this server, certificate provisioning will fail silently.

Step 1: Install Caddy

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
Verify: caddy version should show v2.7+.

Step 2: Create Your Caddyfile

The Caddyfile is Caddy's configuration file. It's intentionally minimal:

# /etc/caddy/Caddyfile

# Simple reverse proxy to a local app
yourdomain.com {
    reverse_proxy localhost:3000

    # Optional: compression and caching
    encode gzip

    # Optional: security headers
    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy strict-origin-when-cross-origin
    }
}

That's it. Save this file and Caddy will:

  1. Provision a Let's Encrypt certificate for yourdomain.com
  2. Set up HTTPS on port 443
  3. Redirect all HTTP traffic to HTTPS
  4. Proxy requests to your app on port 3000
  5. Renew the certificate automatically before it expires

Multiple domains

# /etc/caddy/Caddyfile — multi-site

app1.com {
    reverse_proxy localhost:3000
}

app2.com {
    reverse_proxy localhost:8080
}

api.yourdomain.com {
    reverse_proxy localhost:8888
    encode gzip
}

Static file server

# Serve static files (e.g. a React build)
yourdomain.com {
    root * /var/www/html
    file_server
    encode gzip
}

Step 3: Open Firewall Ports

sudo ufw allow 80/tcp     # HTTP (for Let's Encrypt challenges + redirect)
sudo ufw allow 443/tcp    # HTTPS
sudo ufw reload
Verify: sudo ufw status shows 80 and 443 allowed.

Step 4: Start Caddy

sudo systemctl restart caddy

Caddy will immediately try to provision certificates. This takes 10-30 seconds.

Verify: Open https://yourdomain.com in a browser. You should see a valid HTTPS connection with a padlock icon. If you see a certificate error, check sudo journalctl -u caddy -n 50 --no-pager.

Common Setups

WebSocket proxy

yourdomain.com {
    reverse_proxy localhost:3000 {
        # WebSocket support is automatic in Caddy 2.x
        # Just make sure your backend handles Upgrade headers
    }
}

WebSocket support is built into Caddy's reverse proxy. No extra config needed.

API with path-based routing

yourdomain.com {
    # Frontend
    handle /app/* {
        root * /var/www/html
        file_server
    }

    # API
    handle /api/* {
        reverse_proxy localhost:8888
    }

    # Redirect root to /app
    handle / {
        redir /app/ permanent
    }
}

Custom domain with wildcard cert

# Requires DNS-01 challenge (not covered in free version)
# See the full playbook for Cloudflare DNS setup

*.yourdomain.com {
    reverse_proxy localhost:3000
}

Troubleshooting

"No certificate available" or infinite redirect

Caddy can't verify domain ownership. Check:

  1. DNS: dig yourdomain.com returns your VPS IP
  2. Firewall: port 80 is open (Let's Encrypt uses HTTP-01 challenge)
  3. Caddy is running: sudo systemctl status caddy

Certificate works but backend returns 502

Your backend isn't running on the port you specified. Check:

# Is your app listening?
sudo ss -tlnp | grep 3000

# Check Caddy logs for the exact error
sudo journalctl -u caddy -n 20 --no-pager

Rate limited by Let's Encrypt

You get 5 certificates per week per domain. If you're testing repeatedly:

# Use staging certificates during testing
# Add to your Caddyfile globally:
{
    acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

# Remove this line before going live!

Caddy won't start

# Validate your Caddyfile
caddy validate --config /etc/caddy/Caddyfile

# Check for port conflicts
sudo ss -tlnp | grep -E ':80|:443'

Verification Checklist

CheckCommandExpected
Caddy runningsystemctl is-active caddyactive
HTTPS validOpen in browserPadlock icon, no warnings
HTTP→HTTPS redirectcurl -I http://yourdomain.com301 redirect to https
Backend proxiedcurl -s https://yourdomain.comYour app's response
Certificate auto-renewcaddy list-modules | grep tlstls module loaded
Security headerscurl -sI https://yourdomain.com | grep X-X-Content-Type-Options, X-Frame-Options

Cost

ItemCostNotes
CaddyFreeApache 2.0 license
Let's Encrypt certsFreeAuto-renewed by Caddy
Domain$10/yrAny registrar
Total~$1/moJust the domain

This is the free version. The full Operator Playbook Pack includes multi-domain setup, load balancing, compression tuning, HTTP/3, and DNS-01 challenge configuration.

Get the Full Pack →