How to Install Caddy: The TLS-Automatic Web Server
Install Caddy web server, configure the Caddyfile, get automatic HTTPS from Let's Encrypt, and set up reverse proxying in under 15 minutes.
Before you start
- ▸A server with a public IP address and root or sudo access
- ▸A domain name with an A record pointing to that IP (required for automatic HTTPS)
- ▸Ports 80 and 443 not in use by another service such as Apache or Nginx
Caddy is a production-ready web server written in Go. Its headline feature is automatic HTTPS: give it a domain name and it fetches, renews, and reloads TLS certificates from Let's Encrypt or ZeroSSL without any manual steps. Configuration is a single human-readable file. If you have wrestled with Certbot and Nginx reloads at 3 a.m., Caddy is a welcome change.
Install Caddy
The Caddy project publishes official packages for all major distros. Use the official repo rather than the version in your distro's default repos — it is usually significantly older.
Debian / Ubuntu
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 9 / Rocky Linux 9
sudo dnf install -y 'dnf-command(copr)'
sudo dnf copr enable -y @caddy/caddy
sudo dnf install -y caddy
Arch Linux
sudo pacman -S caddy
Arch ships a reasonably current build from the community repo. For the absolute latest, use the caddy-git AUR package.
Enable and Start the Service
The package creates a caddy system user and installs a systemd unit. Enable it so it starts on boot, then start it now.
sudo systemctl enable --now caddy
Verify it is running:
sudo systemctl status caddy
You should see active (running). Caddy binds to ports 80 and 443 by default. If another service already owns those ports, Caddy will fail to start — stop that service first.
Open the Firewall
Caddy needs inbound HTTP and HTTPS traffic to reach the server. It also needs outbound HTTPS to reach the ACME certificate authority.
ufw (Debian / Ubuntu)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp # HTTP/3 over 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 --permanent --add-port=443/udp
sudo firewall-cmd --reload
Understand the Caddyfile
Caddy's primary configuration format is the Caddyfile, located at /etc/caddy/Caddyfile after a package install. Its syntax is minimal: a block opens with an address, and directives go inside curly braces.
sudo nano /etc/caddy/Caddyfile
A bare-minimum file looks like this:
example.com {
root * /var/www/html
file_server
}
That single block tells Caddy to serve static files from /var/www/html over HTTPS. Caddy will automatically obtain a certificate for example.com the first time a request arrives, as long as the domain resolves to this server's public IP.
Serve a Static Site
Create web root and a test page
sudo mkdir -p /var/www/html
echo '<h1>Hello from Caddy</h1>' | sudo tee /var/www/html/index.html
Set ownership
The caddy process runs as the caddy user. Give it read access to the web root.
sudo chown -R caddy:caddy /var/www/html
Write the Caddyfile
sudo tee /etc/caddy/Caddyfile <<'EOF'
example.com {
root * /var/www/html
file_server
encode gzip
log {
output file /var/log/caddy/access.log
}
}
EOF
Validate and reload
caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
caddy validate parses the file and prints any errors before they affect the running service. Make this a habit before every reload.
Set Up a Reverse Proxy
Caddy's most common production use is fronting an application that listens on a local port — a Node app, a Python API, a Gitea instance, and so on. The reverse_proxy directive handles this in one line and still manages TLS automatically.
sudo tee /etc/caddy/Caddyfile <<'EOF'
app.example.com {
reverse_proxy localhost:3000
}
EOF
If your backend uses its own self-signed TLS, tell Caddy not to verify it:
app.example.com {
reverse_proxy https://localhost:3000 {
transport http {
tls_insecure_skip_verify
}
}
}
For multiple sites in one file, simply stack blocks:
blog.example.com {
root * /var/www/blog
file_server
}
api.example.com {
reverse_proxy localhost:8080
}
app.example.com {
reverse_proxy localhost:3000
}
After each edit, validate and reload:
caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
Automatic HTTPS — How It Works
When Caddy sees a hostname that looks public (not localhost or an IP), it automatically:
- Contacts Let's Encrypt (or ZeroSSL) via the ACME protocol.
- Completes an HTTP-01 or TLS-ALPN-01 challenge to prove domain ownership.
- Stores the certificate and private key under
/var/lib/caddy/.local/share/caddy/. - Renews certificates automatically before expiry, with zero downtime.
- Redirects plain HTTP to HTTPS with a permanent redirect.
For this to work your DNS A record must point to this server's public IP before you start Caddy with that hostname. Let's Encrypt also rate-limits failed attempts, so test with a staging environment if you are iterating quickly:
example.com {
tls {
ca https://acme-staging-v02.api.letsencrypt.org/directory
}
root * /var/www/html
file_server
}
Remove the tls block once everything looks correct to get a real certificate.
Verify TLS and Logs
Check that the certificate was issued and looks correct:
curl -I https://example.com
You should see HTTP/2 200 and no certificate warnings. For more detail:
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>&1 \
| grep -E 'subject|issuer|Verify'
Follow the Caddy journal for real-time diagnostics:
sudo journalctl -u caddy -f
Troubleshooting
- Port 80 or 443 already in use. Run
sudo ss -tlnp | grep -E ':80|:443'to identify the conflicting process, then stop it before starting Caddy. - Certificate not issued, ACME errors in the journal. Confirm your DNS resolves to this server (
dig +short example.com), that ports 80 and 443 are reachable from the internet, and that your firewall rules are applied. - Permission denied reading web root. Verify ownership with
ls -la /var/www/htmland ensure thecaddyuser has at least read and execute on all parent directories. - Config changes have no effect. Confirm you ran
sudo systemctl reload caddy(not restart) and thatcaddy validatereported no errors. - Reverse proxy returns 502. The upstream service is not listening on the expected port. Check it with
sudo ss -tlnp | grep 3000(replace with your port).
Frequently asked questions
- Does Caddy renew certificates automatically, or do I need a cron job?
- Caddy handles renewal entirely by itself. It tracks expiry internally and renews certificates in the background well before they expire, with no cron job or external tool required.
- Can I use Caddy on a server without a public domain name?
- Yes. For localhost or private IP addresses, Caddy automatically issues a locally trusted certificate using its own CA. For production use with a real domain, the domain must resolve to your server's public IP.
- How do I serve multiple domains or subdomains from one Caddy instance?
- Add separate site blocks in the Caddyfile, one per hostname. Caddy obtains and manages a certificate for each domain independently. Reload after saving.
- Does Caddy support HTTP/3?
- Yes. Caddy enables HTTP/3 over QUIC by default on port 443 UDP when HTTPS is active. No extra configuration is needed; just make sure UDP 443 is open in your firewall.
- What is the difference between `systemctl reload caddy` and `systemctl restart caddy`?
- Reload performs a graceful in-place configuration update with zero dropped connections. Restart stops and restarts the process, briefly interrupting active connections. Always prefer reload for config changes.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.