Home/Engineer/Networking / Servers/Caddy Reverse Proxy: Automat…
Networking / ServersIntermediatelinuxcaddyhttpsreverse-proxy

Caddy Reverse Proxy: Automatic HTTPS with Let's Encrypt, Caddyfile Setup, and Zero-Downtime Reloads

A practical operations guide covering Caddy's automatic HTTPS (ACME), Caddyfile syntax, reverse proxying, headers, compression, logging, and zero-downtime configuration via the API.

Contents
  1. What is Caddy?
  2. Installation
  3. Caddyfile Basics
  4. Reverse Proxy
  5. TLS Options
  6. Global Options and Logging
  7. Config Validation and Zero-Downtime Reloads
  8. Admin API
  9. Troubleshooting
  10. Summary

What is Caddy?

Caddy is a web server and reverse proxy written in Go. Its standout feature is automatic HTTPS. Specify a domain and Caddy automatically issues and renews certificates via the ACME protocol (Let's Encrypt / ZeroSSL), redirects HTTP to HTTPS, and even handles OCSP stapling for you. Unlike Nginx or Apache, you don't need a separate certificate tool (certbot) or to worry about cron-based renewals.

The Caddyfile configuration is declarative and concise. Nginx server blocks that run dozens of lines often collapse to just a few lines in Caddy. This guide covers installation, Caddyfile syntax, reverse proxying, TLS options, and the Admin API for zero-downtime reloads, all based on Caddy 2.x.

For certificate issuance, the domain must actually point at this server (A/AAAA records), and ports 80/443 must be reachable from the outside. The ACME HTTP-01 challenge arrives on port 80.

Installation

Bash
# Debian/Ubuntu save
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
caddy version

Installing via apt also creates a systemd service (caddy.service) and a dedicated caddy user. The config file lives at /etc/caddy/Caddyfile.

Bash
sudo systemctl enable --now caddy
systemctl status caddy

Caddyfile Basics

The simplest static site:

CADDY
example.com {
    root * /var/www/example.com
    file_server
    encode gzip zstd
}

Those three lines automatically issue a certificate for example.com, redirect HTTP to HTTPS, and serve static files. The site address on the first line of the block is the certificate issuance target.

You can group multiple domains in a single block.

CADDY
example.com, www.example.com {
    redir https://example.com{uri} permanent
}

Placeholders such as {uri}, {host}, and {remote_host} are available (template variables). Wildcard certificates require a DNS-01 challenge, which means you need to build in a DNS provider plugin.

Reverse Proxy

This is one of Caddy's primary use cases. Put it in front of your backend application.

CADDY
app.example.com {
    reverse_proxy localhost:3000
}

That's all you need for TLS termination plus proxying. Multiple upstreams get automatic load balancing.

CADDY
api.example.com {
    reverse_proxy {
        to localhost:8001 localhost:8002 localhost:8003
 lb_policy round_robin # least_conn, ip_hash
        health_uri /healthz
        health_interval 10s
        health_timeout 3s
    }
}

To route different paths to different backends, use a matcher.

CADDY
example.com {
 # /api/* backend
    reverse_proxy /api/* localhost:8080

 # file
    handle {
        root * /var/www/spa
        try_files {path} /index.html      # SPA fallback
        file_server
    }
}

Forwarding Headers to Upstream

By default, Caddy automatically attaches X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host. Manipulate additional headers like this:

CADDY
reverse_proxy localhost:3000 {
    header_up Host {upstream_hostport}
    header_up X-Real-IP {remote_host}
 header_down -Server # reply Server
}

TLS Options

Leaving automatic HTTPS as-is is recommended, but you can tune it for operational needs.

CADDY
example.com {
 tls admin@example.com { # ACME
        protocols tls1.2 tls1.3
    }
    reverse_proxy localhost:3000
}

To use a private certificate on an internal network/for testing, or to disable issuance:

CADDY
# certificate
tls /etc/ssl/site.crt /etc/ssl/site.key

# CA
tls internal

While testing issuance limits, use Let's Encrypt staging instead of production. Set acme_ca https://acme-staging-v02.api.letsencrypt.org/directory in the global options block to avoid burning through rate limits.

Global Options and Logging

The curly-brace block at the very top of the file is global options.

CADDY
{
    email admin@example.com
 admin localhost:2019 # Admin API
    log {
        output file /var/log/caddy/access.log
        format json
    }
}

example.com {
    reverse_proxy localhost:3000
    log {
        output file /var/log/caddy/example.access.log {
            roll_size 50mb
            roll_keep 10
        }
        format console
    }
}

JSON logs can be parsed directly with jq, which is convenient for observability pipelines.

Config Validation and Zero-Downtime Reloads

Bash
#
caddy validate --config /etc/caddy/Caddyfile
caddy fmt --overwrite /etc/caddy/Caddyfile

# (graceful, )
sudo systemctl reload caddy
# or
caddy reload --config /etc/caddy/Caddyfile

reload applies the new config without killing the process, so in-flight requests are not dropped. That's a major advantage in production.

Admin API

Caddy exposes a REST API on localhost:2019 by default. You can inspect and replace the config as JSON.

Bash
# settings check
curl -s localhost:2019/config/ | jq .

# check
curl -s localhost:2019/reverse_proxy/upstreams | jq .

# settings replace (PATCH)
curl -X POST localhost:2019/load \
  -H "Content-Type: application/json" \
  -d @new-config.json

The Admin API binds to localhost only by default, but it has no authentication. Never expose it to the outside. In container environments, be careful not to bind it to 0.0.0.0.

Troubleshooting

SymptomCause / Check
Certificate issuance failsPort 80 blocked by firewall, A record not set, rate limit
ACME errors in journalctl -u caddyDNS not fully propagated — check with dig example.com
502 Bad GatewayUpstream not running, typo in reverse_proxy port
Config not appliedConfirm you ran reload after caddy validate succeeded
TLS handshake errorCheck protocols setting vs. client compatibility

Always check logs in the systemd journal.

Bash
journalctl -u caddy -f --no-pager

Summary

ItemKey point
Automatic HTTPSSpecify a domain and ACME issues and renews certificates automatically
CaddyfileDeclarative and concise; the site address is the certificate target
Reverse proxyOne-line reverse_proxy; LB and health checks with multiple upstreams
matcherBranch by path/header; use try_files for SPAs
Zero-downtime reloadsystemctl reload caddy / caddy reload
Admin APIlocalhost:2019; do not expose externally

Caddy is a web server built on the philosophy that less configuration is better. Automatic HTTPS and a concise proxy syntax let you stand up an edge gateway for small-to-medium services quickly.

#caddy#https#reverse-proxy#tls#web-server
Editorial note

AI tools may assist drafting. Check the evidence in each guide for whether commands were run and which environment was used. Confirm the current docs for your version before applying instructions, and email us about errors. email us

Topic-related official docs (auto-linked · not per-claim sources)OpenSSL documentation ↗

Questions & answers

Ask a question about this guide. We'll review and reply.