$linuxjunkies
>

Install and Configure Caddy

Install Caddy on Linux, configure auto-HTTPS, set up reverse proxying and static file serving, and extend Caddy with third-party modules — all via the Caddyfile.

BeginnerUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • A Linux server with a public IP address
  • A domain name with an A record pointing to that IP
  • sudo or root access on the server
  • Go 1.21+ installed if you plan to build custom modules with xcaddy

Caddy is a modern web server written in Go that provisions TLS certificates automatically via Let's Encrypt or ZeroSSL — no Certbot, no cron jobs, no manual renewal. Its configuration file, the Caddyfile, is readable enough that you can understand a full reverse-proxy setup at a glance. This guide walks through installation, Caddyfile basics, automatic HTTPS, reverse proxying an app, serving static files, and a few commonly used modules.

Install Caddy

Debian / Ubuntu

The Caddy project maintains an official apt repository. Add it and install:

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 -y caddy

Fedora / RHEL / Rocky

sudo dnf install -y 'dnf-command(copr)'
sudo dnf copr enable @caddy/caddy
sudo dnf install -y caddy

Arch Linux

sudo pacman -S caddy

All three methods install Caddy as a systemd service. Enable and start it immediately:

sudo systemctl enable --now caddy

Confirm the service is running:

systemctl status caddy

You should see active (running). Caddy listens on ports 80 and 443 by default once you add a hostname to the Caddyfile.

Open Firewall Ports

Caddy needs ports 80 (HTTP, used for ACME challenges) and 443 (HTTPS). Open them with whichever firewall tool your distro uses.

ufw (Debian/Ubuntu)

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp   # HTTP/3 (QUIC)
sudo ufw reload

firewalld (Fedora/RHEL/Rocky)

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

nftables (Arch or manual setup)

sudo nft add rule inet filter input tcp dport { 80, 443 } accept
sudo nft add rule inet filter input udp dport 443 accept

Make sure your nftables ruleset is saved to persist across reboots.

The Caddyfile

The main configuration file lives at /etc/caddy/Caddyfile. Its structure is: one or more address blocks, each containing directives. A minimal example:

example.com {
    respond "Hello from Caddy" 200
}

Replace example.com with a real DNS-resolvable hostname pointing to your server, and Caddy will obtain and renew a certificate automatically — no extra steps required.

After editing the Caddyfile, validate it before reloading:

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Use reload, not restart. A reload applies the new config with zero downtime and without dropping existing connections.

Auto-HTTPS in Detail

Caddy enables HTTPS automatically whenever the site address is a domain name (not an IP or localhost). It stores certificates under /var/lib/caddy/.local/share/caddy. A few important points:

  • Port 80 must be reachable from the internet for the HTTP-01 ACME challenge. DNS-01 is available for wildcard certs but requires API credentials for your DNS provider.
  • Caddy defaults to Let's Encrypt with ZeroSSL as a fallback. You can pin a specific CA with the acme_ca global option.
  • For internal or localhost development, Caddy can act as its own CA. Use caddy trust to install the local root into the system store.
# Local HTTPS for development (no real domain needed)
caddy run --config /dev/stdin <<'EOF'
localhost {
    tls internal
    respond "Local HTTPS works" 200
}
EOF

Reverse Proxy

The most common Caddy use case: put it in front of an application running on a local port (Node.js, Gunicorn, a Docker container, etc.).

app.example.com {
    reverse_proxy localhost:3000
}

Caddy handles TLS termination, adds standard proxy headers (X-Forwarded-For, X-Real-IP, X-Forwarded-Proto), and forwards clean HTTP to your app.

Load balancing across multiple backends

app.example.com {
    reverse_proxy localhost:3000 localhost:3001 localhost:3002 {
        lb_policy round_robin
    }
}

Health checks

app.example.com {
    reverse_proxy localhost:3000 {
        health_uri    /healthz
        health_interval 10s
    }
}

WebSocket proxying

Caddy proxies WebSocket connections automatically — no extra directive needed. Just point reverse_proxy at your backend and it works.

File Server

Serving a directory of static files is a single directive:

static.example.com {
    root * /var/www/static
    file_server
}

Enable directory browsing:

static.example.com {
    root * /var/www/static
    file_server browse
}

Combine a file server with a fallback for single-page applications:

spa.example.com {
    root * /var/www/app
    try_files {path} /index.html
    file_server
}

Common Caddyfile Directives

DirectivePurpose
reverse_proxyProxy requests to one or more upstream addresses
file_serverServe static files from a directory
redirIssue HTTP redirects (301/302)
rewriteRewrite request URI internally before handling
headerAdd, remove, or modify response headers
encodeCompress responses (gzip, zstd)
logEnable structured JSON access logging
tlsOverride certificate source or TLS settings
basicauthHTTP Basic authentication gate
rate_limitRequest rate limiting (requires caddy-ratelimit module)

Practical example: secure reverse proxy with headers and compression

api.example.com {
    encode zstd gzip

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options    nosniff
        X-Frame-Options           DENY
        -Server
    }

    log {
        output file /var/log/caddy/api-access.log
    }

    reverse_proxy localhost:8080
}

Third-Party Modules

The stock Caddy binary covers most needs, but modules extend it significantly. Because Caddy is compiled Go, adding a module means building a custom binary. The easiest method is the Caddy download page where you select modules and download a pre-built binary, or use xcaddy:

go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
xcaddy build \
  --with github.com/mholt/caddy-ratelimit \
  --with github.com/caddy-dns/cloudflare

This produces a ./caddy binary in the current directory. Replace the system binary and restart the service. The caddy-dns/cloudflare module enables DNS-01 ACME challenges for wildcard certificates without exposing port 80.

Verify Everything Works

# Check config is valid
sudo caddy validate --config /etc/caddy/Caddyfile

# Check running version and loaded modules
caddy version
caddy list-modules

# Inspect the live config Caddy is actually running
curl -s http://localhost:2019/config/ | python3 -m json.tool | head -40

Port 2019 is Caddy's admin API, bound to localhost only by default. Use it to inspect or hot-reload config without touching the Caddyfile.

# Test HTTPS certificate
curl -I https://example.com

Look for HTTP/2 200 and a valid Strict-Transport-Security header if you added it.

Troubleshooting

  • Certificate not issued: Check that port 80 is publicly reachable (curl http://your-domain/ from an external machine) and that your DNS A record points to this server. Caddy logs ACME errors to the journal: sudo journalctl -u caddy -f.
  • Permission denied on ports 80/443: The packaged Caddy binary has the CAP_NET_BIND_SERVICE capability set. If you replaced the binary with a custom xcaddy build, run sudo setcap cap_net_bind_service=+ep ./caddy before copying it into place.
  • 502 Bad Gateway: Your upstream app isn't listening on the expected port. Verify with ss -tlnp | grep 3000 (replace 3000 with your port).
  • Config changes not taking effect: Make sure you ran sudo systemctl reload caddy, not just saved the file. Confirm with curl -s http://localhost:2019/config/.
  • Rate limit or header directives not recognized: These may require a module not present in the stock binary. Run caddy list-modules | grep rate to check.
tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

Frequently asked questions

Do I need Certbot or any other tool to get HTTPS with Caddy?
No. Caddy handles certificate issuance and renewal internally. As long as your domain resolves to the server and port 80 is reachable, HTTPS is fully automatic with no external tooling.
How do I get a wildcard TLS certificate with Caddy?
Wildcard certs require DNS-01 validation. Install the appropriate caddy-dns module for your DNS provider using xcaddy, add your API credentials to the Caddyfile's tls block, and use `*.example.com` as your site address.
Can Caddy run alongside Nginx or Apache on the same server?
Not on the same ports. You can run Caddy as a reverse proxy in front of Nginx or Apache by having Caddy bind ports 80/443 and forwarding traffic to the other server on a private port like 8080.
Where does Caddy store its TLS certificates?
Certificates are stored under `/var/lib/caddy/.local/share/caddy` when running as the caddy system user. Never delete this directory or you may hit Let's Encrypt rate limits on re-issuance.
Is the Caddy admin API safe to leave running?
By default the admin API binds only to `localhost:2019`, so it is not externally accessible. Do not expose it publicly without authentication. You can disable it entirely with `admin off` in the global options block if you don't need hot-reload.

Related guides