Caddy Reverse Proxy Guide 2026: Automatic HTTPS Made Easy
TUTORIAL 12 min read fordnox

Caddy Reverse Proxy Guide 2026: Automatic HTTPS Made Easy

Set up Caddy as a reverse proxy with automatic HTTPS, zero-config SSL, and simple Caddyfile syntax. Complete VPS deployment guide.


Caddy Reverse Proxy Guide: Automatic HTTPS Made Easy

Caddy is the web server that does HTTPS by default. No certbot, no cron jobs, no renewal scripts. Point a domain at Caddy and it gets a certificate automatically.

What is Caddy?

What is Caddy?

What is Caddy?

Caddy sits between the internet and your services:

Internet → Caddy → Service A (app.domain.com)
                 → Service B (api.domain.com)
                 → Service C (admin.domain.com)

Why developers love Caddy:

Caddy vs Nginx vs Traefik

FeatureCaddyNginxTraefik
Auto HTTPSDefaultCertbot neededBuilt-in
Config syntaxSimpleModerateComplex
Docker integrationGoodManualNative
PerformanceExcellentExcellentExcellent
PluginsGo modulesC modulesMiddleware
Config reloadZero-downtimeRequires signalHot reload
Learning curveLowModerateHigher

Caddy wins on simplicity. If you want the easiest reverse proxy setup, this is it. See our Traefik Docker guide for container-native routing.

VPS Requirements

Caddy is extremely lightweight:

Your VPS sizing depends on your backend services, not Caddy. Check our best VPS for Docker guide for sizing recommendations.

Install Caddy

Option 1: Package Manager (Debian/Ubuntu)

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 caddy

Option 2: Docker

# docker-compose.yml
services:
  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
      - 443:443/udp  # HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

Option 3: Single Binary

curl -sS https://webi.sh/caddy | sh

Basic Reverse Proxy

The Simplest Caddyfile

app.domain.com {
    reverse_proxy localhost:3000
}

That’s it. Caddy will:

Multiple Services

app.domain.com {
    reverse_proxy localhost:3000
}

api.domain.com {
    reverse_proxy localhost:8080
}

admin.domain.com {
    reverse_proxy localhost:9000
}

Each domain gets its own certificate automatically.

Docker Reverse Proxy Setup

Step 1: Create Project Directory

mkdir -p /opt/caddy
cd /opt/caddy

Step 2: Docker Compose

# docker-compose.yml
services:
  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
      - 443:443/udp
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - caddy

  whoami:
    image: traefik/whoami
    container_name: whoami
    networks:
      - caddy

networks:
  caddy:
    name: caddy

volumes:
  caddy_data:
  caddy_config:

Step 3: Caddyfile

test.domain.com {
    reverse_proxy whoami:80
}

Step 4: Launch

docker compose up -d

Visit https://test.domain.com — it works with full HTTPS.

Adding More Docker Services

Any service on the caddy network can be proxied:

# separate docker-compose.yml
services:
  myapp:
    image: myapp:latest
    container_name: myapp
    networks:
      - caddy

networks:
  caddy:
    external: true

Then add to your Caddyfile:

myapp.domain.com {
    reverse_proxy myapp:8080
}

Reload Caddy:

docker exec caddy caddy reload --config /etc/caddy/Caddyfile

Path-Based Routing

Route by Path

domain.com {
    handle /api/* {
        reverse_proxy localhost:8080
    }

    handle /admin/* {
        reverse_proxy localhost:9000
    }

    handle {
        reverse_proxy localhost:3000
    }
}

Strip Path Prefix

domain.com {
    handle_path /api/* {
        reverse_proxy localhost:8080
    }
}

handle_path strips the matched prefix. /api/users becomes /users for the backend.

Load Balancing

Round Robin (Default)

app.domain.com {
    reverse_proxy app1:3000 app2:3000 app3:3000
}

With Health Checks

app.domain.com {
    reverse_proxy app1:3000 app2:3000 {
        health_uri /health
        health_interval 10s
        health_timeout 5s
    }
}

Sticky Sessions

app.domain.com {
    reverse_proxy app1:3000 app2:3000 {
        lb_policy cookie
    }
}

Least Connections

app.domain.com {
    reverse_proxy app1:3000 app2:3000 {
        lb_policy least_conn
    }
}

Security

Basic Authentication

# Generate password hash
caddy hash-password --plaintext 'your-secure-password'
admin.domain.com {
    basicauth {
        admin $2a$14$Zkx19...hashed...password
    }
    reverse_proxy localhost:9000
}

IP Allowlist

admin.domain.com {
    @blocked not remote_ip 192.168.1.0/24 10.0.0.0/8
    respond @blocked 403

    reverse_proxy localhost:9000
}

Rate Limiting

api.domain.com {
    rate_limit {
        zone dynamic {
            key {remote_host}
            events 100
            window 1m
        }
    }
    reverse_proxy localhost:8080
}

Security Headers

domain.com {
    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"
        -Server
    }
    reverse_proxy localhost:3000
}

The -Server directive removes the Server header from responses.

Wildcard Certificates

For *.domain.com, use DNS challenge:

*.domain.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }

    @app host app.domain.com
    handle @app {
        reverse_proxy localhost:3000
    }

    @api host api.domain.com
    handle @api {
        reverse_proxy localhost:8080
    }
}

Build Caddy with the Cloudflare DNS module:

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

Or use the Docker image with modules:

services:
  caddy:
    image: caddy:2-builder AS builder
    # Use multi-stage or pre-built custom image

Static Files + API

SPA with API Backend

domain.com {
    handle /api/* {
        reverse_proxy localhost:8080
    }

    handle {
        root * /srv/frontend
        try_files {path} /index.html
        file_server
    }
}

Serve Static + Proxy

domain.com {
    root * /var/www/html
    file_server

    handle /app/* {
        reverse_proxy localhost:3000
    }
}

WebSocket Support

Caddy proxies WebSocket connections automatically:

ws.domain.com {
    reverse_proxy localhost:8080
}

No extra configuration needed. Caddy detects the Upgrade header and handles it.

Logging

Access Logs

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

Per-Site Logging

app.domain.com {
    log {
        output file /var/log/caddy/app.log
    }
    reverse_proxy localhost:3000
}

api.domain.com {
    log {
        output file /var/log/caddy/api.log
    }
    reverse_proxy localhost:8080
}

Compression

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

Caddy supports both gzip and Zstandard compression out of the box.

CORS Headers

api.domain.com {
    header Access-Control-Allow-Origin "https://app.domain.com"
    header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
    header Access-Control-Allow-Headers "Content-Type, Authorization"

    @options method OPTIONS
    respond @options 204

    reverse_proxy localhost:8080
}

Redirects

WWW to Apex

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

domain.com {
    reverse_proxy localhost:3000
}

HTTP to HTTPS

Caddy does this automatically. No configuration needed.

Custom Redirects

domain.com {
    redir /old-page /new-page permanent
    redir /blog/* /articles/{re.1} permanent
    reverse_proxy localhost:3000
}

Caching

domain.com {
    header /static/* Cache-Control "public, max-age=31536000, immutable"
    header /api/* Cache-Control "no-cache"
    reverse_proxy localhost:3000
}

Configuration via API

Caddy has a full REST API for config changes:

# Get current config
curl localhost:2019/config/

# Update a route
curl localhost:2019/config/apps/http/servers/srv0/routes/0 \
  -X PUT \
  -H "Content-Type: application/json" \
  -d '{"handle": [{"handler": "reverse_proxy", "upstreams": [{"dial": "localhost:3001"}]}]}'

# Reload from Caddyfile
caddy reload --config /etc/caddy/Caddyfile

Troubleshooting

Certificate Not Issued

  1. Check DNS points to your VPS:
dig +short domain.com
  1. Ensure ports 80 and 443 are open:
sudo ufw allow 80,443/tcp
  1. Check Caddy logs:
journalctl -u caddy --no-pager -n 50
# Or for Docker:
docker logs caddy

502 Bad Gateway

  1. Verify backend is running:
curl localhost:3000
  1. Check the upstream address in Caddyfile

  2. For Docker: ensure both containers are on the same network

Config Syntax Error

caddy validate --config /etc/caddy/Caddyfile

Slow Performance

  1. Enable compression:
encode gzip zstd
  1. Check backend response times

  2. Add connection timeouts:

reverse_proxy localhost:3000 {
    transport http {
        dial_timeout 5s
        response_header_timeout 10s
    }
}

Production Checklist

Best VPS for Caddy

Caddy runs anywhere. Pick your VPS based on your backend needs:

ProviderPlanPriceBest For
HostingerKVM1$4.99Best value starter
HetznerCX22€5.49Europe-based projects
DigitalOceanBasic$6Developer-friendly
VultrVC2$6Global edge locations

Hostinger offers the best bang for your buck if you’re starting out. Their KVM1 plan handles Caddy plus several backend services comfortably.

FAQ

Is Caddy production-ready?

Absolutely. Caddy v2 powers thousands of production sites. It’s used by companies of all sizes.

Caddy vs Nginx for reverse proxy?

Caddy for simplicity and automatic HTTPS. Nginx for maximum control and legacy configurations. For most new deployments, Caddy saves hours of setup time.

How does automatic HTTPS work?

Caddy uses the ACME protocol to get certificates from Let’s Encrypt (or ZeroSSL). It handles issuance, renewal, and OCSP stapling automatically.

Can Caddy replace Apache?

Yes. Caddy handles everything Apache does for modern web deployments, with a fraction of the config.

Does Caddy support HTTP/3?

Yes, HTTP/3 (QUIC) is enabled by default on the HTTPS port.

How do I update Caddy?

Package manager: apt upgrade caddy. Docker: pull the new image. Binary: download and replace.

Summary

Caddy makes reverse proxying simple:

TaskNginxCaddy
Get SSLInstall certbot, configureAutomatic
Add serviceEdit config, test, reloadAdd 3 lines, reload
Redirect HTTP→HTTPSAdd server blockAutomatic
Enable HTTP/2ConfigureAutomatic
Config syntaxComplexHuman-readable

Three lines in a Caddyfile replace dozens in Nginx config. For new projects, Caddy is the fastest path to a secure, production-ready reverse proxy. Pair it with Docker Compose for a complete deployment workflow, or check out our VPS hardening guide to lock down the rest of your server.

~/caddy-reverse-proxy-guide/get-started

Ready to get started?

Get the best VPS hosting deal today. Hostinger offers 4GB RAM VPS starting at just $4.99/mo.

Get Hostinger VPS — $4.99/mo

// up to 75% off + free domain included

// related topics

caddy reverse proxy caddy server setup caddy ssl caddy docker caddy vs nginx

// related guides

Andrius Putna

Andrius Putna

I am Andrius Putna. Geek. Since early 2000 in love tinkering with web technologies. Now AI. Bridging business and technology to drive meaningful impact. Combining expertise in customer experience, technology, and business strategy to deliver valuable insights. Father, open-source contributor, investor, 2xIronman, MBA graduate.

// last updated: March 11, 2026. Disclosure: This article may contain affiliate links.