Skip to content
brainNotFound

Server Setups/Getting started

Terminate TLS with Caddy in front of Node

Automatic HTTPS, HTTP/3 and sane security headers in a twelve-line Caddyfile — no certbot cron jobs.

beginner15 min

// read first

On this page
  1. Install
  2. The Caddyfile
  3. Verify
  4. Common failures

Caddy exists to make this boring. It obtains and renews certificates by itself, defaults to sensible TLS settings, and its config file is short enough to read in full.

I have run nginx with certbot for years and there is nothing wrong with it. There is simply no longer a reason to, for a single-host setup.

Install

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
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

The Caddyfile

This is a complete production config. The encode line matters more than it looks — without it you are shipping uncompressed HTML.

/etc/caddy/Caddyfilenginx
example.com, www.example.com {
	encode zstd gzip

	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
		X-Content-Type-Options    "nosniff"
		Referrer-Policy           "strict-origin-when-cross-origin"
		-Server
	}

	handle_path /static/* {
		root * /home/deploy/app/public
		file_server
		header Cache-Control "public, max-age=31536000, immutable"
	}

	reverse_proxy 127.0.0.1:3000
}

Verify

  1. sudo caddy validate --config /etc/caddy/Caddyfile — catches syntax errors before a reload.
  2. sudo systemctl reload caddy — reloads without dropping connections.
  3. curl -sI https://example.com | head -20 — confirm a 200 and the HSTS header.
  4. journalctl -u caddy -f — watch the certificate being issued.

Common failures

SymptomCauseFix
Cert never issuesPort 80 blockedufw allow 80/tcp — ACME needs it
502 from CaddyApp not listening on 127.0.0.1Bind to loopback, not 0.0.0.0
Stale assetsImmutable cache on hashed files onlyOnly set immutable on fingerprinted paths
What goes wrong, and why