I picked up a cheap Debian 13 VPS (2 vCPU, 2 GB RAM, 35 GB disk) to host a few static sites. The goal was something I could forget about: automatic certificates, automatic security updates, and no SSH-and-edit to change a page.

The stack

  • Caddy serves every site from its own directory and gets a Let’s Encrypt certificate per hostname on its own. A shared snippet handles compression, security headers, and access logs.
  • Cloudflare proxies everything, in Full (strict) mode so it verifies Caddy’s certificate.
  • nftables drops everything except SSH, and only accepts web traffic from Cloudflare’s ranges. A weekly systemd timer refreshes those ranges into a named set.
  • unattended-upgrades handles Debian security updates.

Nothing runs in Docker. With two or three services, native packages and systemd units are less to reason about, and Docker’s own firewall rules would bypass the input chain anyway.

Deploys

Each site is a Hugo repo on GitHub. A workflow builds on push and once a day, then rsyncs public/ to the server. The daily build lets date-driven content update itself even when nobody commits.

The deploy key is the part I like most. On the server it’s restricted in authorized_keys:

restrict,command="/usr/bin/rrsync /srv/www/example" ssh-ed25519 AAAA... github-deploy

rrsync ships with Debian’s rsync package. With that forced command the key can’t open a shell or forward ports, and it can only write inside that one directory. If the GitHub secret ever leaked, the worst case is a defaced page, not a compromised server.

Small things that bit me

  • Running sudo caddy validate created the access log as root, and the next reload failed with permission denied. Validate as the service user instead: sudo -u caddy caddy validate ....
  • GitHub stopped accepting passwords for git over HTTPS in 2021. On a fresh laptop, gh auth login works, but git only uses it after gh auth setup-git.
  • Bringing a domain up DNS-only first, so Let’s Encrypt can reach the origin directly, leaves the origin IP in passive-DNS history. With HTTP-01 working through the proxy, it’s better to start proxied.