Deployment guide

Caddy as a reverse proxy on VPS: complete guide

Deploy on a VPS Cloud →

Tutorial

Caddy as a reverse proxy on VPS: complete guide

Deployment8 min read7 steps

Caddy is a Go-based web server that handles the entire TLS lifecycle — ACME negotiation, renewal, HTTP → HTTPS redirect — with no dedicated configuration required. Where Nginx requires Certbot, a cron job and a separate vhost per domain, a single ten-line `Caddyfile` is enough to expose multiple applications on the same VPS over HTTPS. This guide covers installation, multi-app configuration, hardening and the most common errors.

Contents· Why choose Caddy as a reverse proxy on your VPS?1/9
  1. 01Why choose Caddy as a reverse proxy on your VPS?
  2. 02What Caddy brings to production
  3. 03Prerequisites
  4. 04Deploying Caddy as a reverse proxy
  5. 05How Caddy's automatic HTTPS works
  6. 06Caddy vs Nginx: when to choose which
  7. 07Troubleshooting: common errors
  8. 08For hosting multiple applications on the same VPS
  9. 09Official documentation

Why choose Caddy as a reverse proxy on your VPS?

Most reverse proxies delegate TLS management to an external tool — Certbot, acme.sh or a homemade cron script. Caddy integrates this mechanism end-to-end: as soon as a domain name is declared in the Caddyfile, it contacts Let's Encrypt, obtains the certificate and renews it before expiry, without manual intervention. The result is a configuration that reads in a few lines and deploys identically across all your VPS instances, with no external state to synchronise.

What Caddy brings to production

  • Automatic HTTPS: Let's Encrypt ACME HTTP-01 activated as soon as a domain name is declared — no cron, no plugin to install
  • Concise syntax: a five-line block replaces a forty-line Nginx vhost with its Certbot configuration file
  • Zero cold reload: caddy reload applies the new Caddyfile without dropping in-flight connections
  • HTTP/2 and HTTP/3 (QUIC) by default: enabled without additional configuration on all 2.x versions
  • JSON API: configuration can be updated live via a REST API, useful for dynamic or containerised environments
  • Lightweight official Docker image: caddy:2-alpine weighs under 20 MB and covers most production cases

Prerequisites

Before you start, verify that your VPS meets the following requirements.

Minimum resources: 512 MB of RAM and 1 vCPU are sufficient for Caddy alone. Allow 1 GB RAM and 2 vCPU if you host several applications behind it.

Ports 80 and 443 free: Caddy occupies them for ACME HTTP-01 (port 80) and for HTTPS traffic (port 443). Check that no other process is using them:

ss -tlnp | grep -E ':80|:443'

Domain name pointing to the VPS: Let's Encrypt validates domain ownership by querying port 80 on your IP. Your DNS A record must be active and propagated before starting Caddy.

Operating system: Debian 11/12 or Ubuntu 22.04/24.04. Commands in this guide are tested on these distributions.

Docker (optional): if you deploy Caddy in a container, Docker Engine 24+ and Docker Compose v2 are required.

Deploying Caddy as a reverse proxy

  1. Method 1 — Installation via the official APT repository

    Caddy provides a signed APT repository. Add it, then install the package:

    apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
      | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
      | tee /etc/apt/sources.list.d/caddy-stable.list
    apt update && apt install caddy

    Verify the installed version: caddy version should display v2.9.x or higher.

  2. Method 2 — Deployment with Docker Compose

    If your applications already run under Docker, a Caddy service in the same compose.yml is the simplest approach. Create a compose.yml file at the root of your project:

    services:
      caddy:
        image: caddy:2-alpine
        restart: unless-stopped
        ports:
          - "80:80"
          - "443:443"
          - "443:443/udp"
        volumes:
          - ./Caddyfile:/etc/caddy/Caddyfile
          - caddy_data:/data
          - caddy_config:/config
        networks:
          - proxy
    
    volumes:
      caddy_data:
      caddy_config:
    
    networks:
      proxy:
        external: true

    Create the shared network once: docker network create proxy. Your other services join this network to be accessible by Caddy.

  3. Create the basic Caddyfile

    The Caddyfile is the central configuration file. For an APT installation, it lives at /etc/caddy/Caddyfile. For Docker, place it next to your compose.yml.

    Here is a minimal example that exposes an application on port 3000:

    myapp.com {
        reverse_proxy localhost:3000
    }

    That's it. Caddy detects that myapp.com is a domain name, contacts Let's Encrypt via ACME HTTP-01, obtains a certificate and automatically redirects all HTTP traffic to HTTPS. Certificates are stored in ~/.local/share/caddy/ (APT) or in the caddy_data volume (Docker).

  4. Multi-application configuration (multi-vhost)

    Hosting multiple applications on the same VPS only requires additional blocks in the same file:

    app1.yourdomain.com {
        reverse_proxy localhost:3000
    }
    
    app2.yourdomain.com {
        reverse_proxy localhost:4000
    }
    
    blog.yourdomain.com {
        reverse_proxy localhost:8080
    }

    Each block gets its own Let's Encrypt certificate. Caddy manages renewals independently for each domain, in parallel, without service interruption.

  5. Apply the Caddyfile without interruption

    For APT, reload the configuration without dropping connections:

    systemctl reload caddy
    # or, if you modified the file outside /etc/caddy/:
    caddy reload --config /path/to/Caddyfile

    For Docker:

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

    Verify that the syntax is correct before reloading: caddy validate --config /etc/caddy/Caddyfile.

  6. Add security headers and rate limiting

    Caddy supports native security headers via the header directive. Here is a hardened block for a production application:

    myapp.com {
        reverse_proxy localhost:3000
    
        header {
            Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
            X-Content-Type-Options "nosniff"
            X-Frame-Options "SAMEORIGIN"
            Referrer-Policy "strict-origin-when-cross-origin"
            -Server
        }
    
        log {
            output file /var/log/caddy/myapp.log {
                roll_size 10mb
                roll_keep 5
            }
        }
    }

    For rate limiting, the caddy-ratelimit module is installed via xcaddy build and used with the rate_limit directive. The official Docker image does not include it — build your own image or use an upstream application firewall if the need is critical.

  7. Enable and verify the systemd service (APT)

    After installation via APT, the service is enabled automatically. Check its status and logs:

    systemctl status caddy
    journalctl -u caddy -f

    To ensure Caddy starts at reboot: systemctl enable caddy (already done by the package). Test that the Let's Encrypt certificate was successfully issued:

    curl -sv https://myapp.com 2>&1 | grep -E 'subject|issuer|expire'

How Caddy's automatic HTTPS works

Caddy implements the ACME (Automated Certificate Management Environment) protocol natively. As soon as a Caddyfile block contains a qualified domain name, Caddy automatically triggers the ACME HTTP-01 challenge: Let's Encrypt places a verification token at http://yourdomain.com/.well-known/acme-challenge/, Caddy responds to this request and proves it controls the server. Let's Encrypt then issues a certificate valid for 90 days.

Caddy renews this certificate in advance (starting 30 days before expiry) and reloads the configuration hot, without service interruption. Certificates and ACME data are stored locally — in ~/.local/share/caddy/ for an APT installation, in the caddy_data volume for Docker. Do not delete this volume: Caddy also stores ACME state there, and too many requests to Let's Encrypt trigger rate limits that can block new certificate issuance for several hours.

For wildcard certificates (*.yourdomain.com), ACME HTTP-01 does not work: the DNS-01 challenge is required, which needs access to your DNS provider's API. Caddy natively supports several providers via modules (caddy-dns/cloudflare, caddy-dns/ovh…) compiled with xcaddy.

Caddy vs Nginx: when to choose which

Scroll the table

CriterionCaddyNginx
TLS configurationAutomatic, zero manual configurationManual (Certbot or acme.sh required)
Learning curveLow — readable syntax, few directivesModerate — verbose syntax, many contexts
Raw performance (req/s)Excellent for most workloadsSlightly higher under very high load
Modules and ecosystemGrowing, xcaddy to compile modulesVery large, numerous mature third-party modules
Reload without interruptionNative (`caddy reload`)Native (`nginx -s reload`), similar
Ideal use casePersonal VPS, team projects, Docker microservicesLarge platforms, CDN frontend, advanced cases (GeoIP, Lua…)
HTTP/3 (QUIC) supportEnabled by default since Caddy 2.6Experimental, requires compilation with quiche or ngx_http_v3

Troubleshooting: common errors

Port 80 already in use. Caddy needs port 80 for ACME HTTP-01. If Apache or Nginx is already running, stop it before starting Caddy: systemctl stop apache2 or systemctl stop nginx. Then verify with ss -tlnp | grep :80.

ACME failure / certificate not issued. Let's Encrypt cannot reach your server. Check: (1) your DNS A record points to the VPS IP — dig +short your-domain.com must return your IP; (2) port 80 is accessible from the outside — VPS firewalls (iptables, ufw) sometimes block this port even when Caddy is listening. Check Caddy logs with journalctl -u caddy to read the precise ACME error message.

permission denied on ports 80/443. Ports below 1024 are reserved under Linux. For an APT installation, the package automatically configures the CAP_NET_BIND_SERVICE capability on the Caddy binary. If you compiled Caddy manually, apply it: setcap cap_net_bind_service=+ep $(which caddy).

DNS not resolved / no such host. Caddy resolves the names of backends declared in reverse_proxy. If your application is called app in Docker Compose, make sure Caddy and the application share the same Docker network. Verify with: docker compose exec caddy nslookup app.

caddy_data volume accidentally deleted. Caddy must renegotiate all certificates from scratch. Let's Encrypt enforces a limit of 5 certificates per domain over 7 days. If you hit this limit, use the ACME staging environment (acme_ca https://acme-staging-v02.api.letsencrypt.org/directory in the Caddyfile) to test, then switch back to production once the window has passed.

For hosting multiple applications on the same VPS

Create a shared Docker network (docker network create proxy) and connect each service to that network. The Caddyfile then references services by their container name rather than localhost:PORT — the configuration does not change when you add or remove applications. Example: reverse_proxy my-service:3000 instead of reverse_proxy localhost:3000.

Official documentation

The complete reference for directives, modules and the JSON API is available at <a href="https://caddyserver.com/docs/">caddyserver.com/docs</a>. The community forum (<a href="https://caddy.community">caddy.community</a>) is the go-to community reference for advanced questions — the maintainers reply directly.

Your VPS, ready for Caddy

Deploy Caddy on a ServOrbit VPS and get a Debian 12 environment, ports 80 and 443 open, and immediate SSH access.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab