Back to blog
August 19, 2026Guides

How to Install Caddy Web Server with Automatic HTTPS

Install Caddy on Ubuntu from the official repository, write a Caddyfile for static sites and reverse proxies, understand automatic certificates, systemd, logs, and common mistakes when migrating from Nginx.

How to Install Caddy Web Server with Automatic HTTPS

Caddy is a web server that obtains and renews TLS certificates by default. For a small VPS that hosts one or two domains, that removes a whole class of Certbot timers and Nginx snippet files. The configuration language (Caddyfile) is short. Reverse proxy, gzip, and HTTP/2 are normal features, not a weekend of extra modules.

Nginx is still the right choice for some shops: you already have battle-tested configs, or you need a very specific module. This guide is for the other common case — you want HTTPS that works on a fresh Hiddence VPS without memorizing include snippets. We will install Caddy from the official apt repo, explain how it talks to Let's Encrypt, serve a static site, proxy a local app, host several domains, look at logs, and cover the usual port-80 fights with Apache or an old Nginx.

When Caddy is a good fit

Automatic HTTPS is the headline, but the daily win is fewer moving parts. Caddy listens on 80 and 443, redirects HTTP to HTTPS, and stores certs in its data directory. You still need a domain pointing at the VPS. You still need to not break ACME (firewall 80/tcp, no other process stealing :80). Wildcard certificates need a DNS provider module — that is a longer path than a single-host cert.

  • Automatic HTTP→HTTPS and certificate renewal
  • Readable Caddyfile instead of long server blocks
  • Capable reverse proxy for Node, Python, PHP-FPM via extra config, or Docker backends
  • HTTP/2 and modern TLS defaults without a cipher spreadsheet
  • systemd unit from the official package

Requirements

A domain must resolve to this VPS before Caddy can prove ACME HTTP-01. If DNS is still propagating, Caddy will fail issuance and retry; that looks like 'Caddy is broken' when it is only DNS. Stop Apache or Nginx first if they own 80/443.

  • Ubuntu 22.04 or 24.04
  • A domain A record to the VPS IP (and AAAA if you use IPv6)
  • Ports 80 and 443 free and allowed in the firewall
  • Root or sudo

Step 1: Install Caddy from the official repository

Do not install a random old Caddy from the default Ubuntu universe if you want current TLS and ACME behaviour. The Caddy project documents an apt source. After install, the caddy user exists and the service is enabled.

bash
ssh root@YOUR_VPS_IP
apt update && apt -y install debian-keyring debian-archive-keyring apt-transport-https curl gpg

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

caddy version
systemctl status caddy --no-pager

Step 2: First Caddyfile — static site

The default Caddyfile is /etc/caddy/Caddyfile. Replace it with your domain. Caddy will try to get a certificate as soon as the config loads if the hostname is not localhost. Put site files in a directory readable by the caddy user (often www-data-style permissions, but the package uses user caddy).

bash
mkdir -p /var/www/example
echo '<h1>It works</h1>' > /var/www/example/index.html
chown -R caddy:caddy /var/www/example

cat >/etc/caddy/Caddyfile <<'EOF'
example.com {
    root * /var/www/example
    file_server
    encode gzip
}
EOF

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

# Watch issuance:
journalctl -u caddy -f

Step 3: Reverse proxy a local application

If Gunicorn, Node, or Docker listens on 127.0.0.1:8000, Caddy should be the only public process. The reverse_proxy directive forwards Host and X-Forwarded-* headers in a sensible way for most apps. If the app generates absolute HTTP URLs, set the trusted proxy / HTTPS flags in the app (Django SECURE_PROXY_SSL_HEADER, Express trust proxy, and so on).

bash
app.example.com {
    encode gzip
    reverse_proxy 127.0.0.1:8000
}

# Several hosts in one file are normal:
# blog.example.com {
#     root * /var/www/blog
#     file_server
# }

Step 4: Firewall and systemd

Allow 80 and 443. Caddy's unit is caddy.service; reload is enough after Caddyfile edits if the process is healthy. If you change environment variables for a DNS plugin, a full restart is clearer. Certificates live under /var/lib/caddy/.local/share/caddy/ by default — include that path in backups if you care about rate limits during a reinstall.

bash
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

systemctl enable --now caddy
systemctl reload caddy

# Backup cert storage (path may vary slightly by version):
ls -la /var/lib/caddy/

Step 5: Logs, compression and headers

Access logs help when a bot hammers a path or when you debug 404s. You can log per site. Add security headers if you host a browser app; do not copy a huge header pack without understanding HSTS (once you set a long max-age, browsers remember it).

bash
example.com {
    root * /var/www/example
    file_server
    encode gzip
    log {
        output file /var/log/caddy/example.log
    }
    header {
        X-Content-Type-Options nosniff
        Referrer-Policy no-referrer-when-downgrade
        -Server
    }
}

mkdir -p /var/log/caddy
chown caddy:caddy /var/log/caddy
systemctl reload caddy

Step 6: PHP and other extras

Caddy can talk to PHP-FPM with the php_fastcgi directive. That is enough for many WordPress or Laravel hosts, but you still need php-fpm installed and a socket path that matches. If you already have a perfect Nginx PHP config, migrating in one evening is optional — Caddy shines more on reverse proxy + static + automatic TLS.

bash
example.com {
    root * /var/www/example
    php_fastcgi unix//run/php/php8.3-fpm.sock
    file_server
}

# Confirm FPM is running:
systemctl status php8.3-fpm

Migrating from Nginx without downtime surprises

Stop Nginx before starting Caddy or they will fight for 80/443. Lower DNS TTL the day before if you are also moving IPs. Test with curl --resolve so you can hit the new VPS before switching the A record. Keep Nginx configs in git for a week in case you need to roll back.

  • systemctl stop nginx && systemctl disable nginx
  • Install Caddy, validate Caddyfile, start Caddy
  • curl -I --resolve example.com:443:NEW_IP https://example.com
  • Only then change DNS if the IP is new
  • Re-issue is automatic; do not also run Certbot against the same hostname

Troubleshooting

ACME failures are almost always DNS, firewall, or another service on port 80. Caddy log lines mention tls.obtain. If the site works on HTTP but not HTTPS, issuance never completed. If you see too many certificates, you hit Let's Encrypt rate limits — use the staging CA while testing, not production.

  • validate fails: Caddyfile syntax, missing brace
  • permission denied on root: chown caddy for the site directory
  • bind: address already in use — ss -tulpn | grep -E ':80|:443'
  • certificate timeout: dig +short the domain from the VPS, ufw allow 80
  • 502 reverse_proxy: backend down, or you proxied to localhost from a container network incorrectly

Security notes

Caddy's TLS defaults are conservative. Your job is application security and SSH. Do not enable the Caddy admin API on a public interface. The default admin endpoint is local; leave it that way. If you use a Caddyfile from the internet, read every matcher — a snippet that reverse_proxies /* to an internal IP can become an open proxy.

  • Do not expose the admin API
  • Keep the package updated
  • HSTS only after you are sure HTTPS works for all hostnames
  • Separate test and production hostnames to protect ACME rate limits

Tips

  • caddy fmt --overwrite /etc/caddy/Caddyfile keeps the file readable
  • Use import snippets when you have many similar sites
  • For Docker, the caddy:alpine image with a mounted Caddyfile is common
  • Wildcard certs need a DNS module and API token — protect that token
  • Read journalctl -u caddy before you rewrite the whole file

Caddy on a VPS is: official package, a short Caddyfile, ports 80/443 open, DNS already pointing at the server, and systemd reload after edits. Use it as a static file server or as a reverse proxy in front of Gunicorn, Node, or Docker. Automatic HTTPS works when ACME can answer on port 80; if issuance fails, fix DNS and port conflicts before you blame Caddy. Back up /var/lib/caddy if you reinstall often.