seyshee.dev

Self-hosting a tiny static site with nginx

This site used to be a single index.html. It is now a few dozen HTML files produced by a Python script, and that's about as complicated as I want it to get. Here is the whole setup, mostly so that I remember it next time.

The build

There is no framework. A ~300-line build.py reads Markdown files with a tiny front matter block, renders them into one HTML template and writes everything into dist/. It also packs project tarballs and makes thumbnails for the gallery. The only dependency is Pillow.

I tried Hugo and liked it, but every time I came back after six months the theme had changed its config format. A script I wrote myself doesn't do that.

Deploying

Deploy is rsync to a VPS with a backup of the previous version:

ssh host "rsync -a /var/www/site/ /root/site-backups/site-$(date +%F-%H%M)/"
rsync -rlt --delete --chmod=D755,F644 dist/ host:/var/www/site/

Rollback is the same command in the other direction. I keep the last ten backups; the whole site is a few dozen megabytes, mostly images.

nginx

The server block is short. The interesting part is cache policy:

location / {
    try_files $uri $uri/ =404;
}
location /static/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}
location ~* \.(jpg|png|webp|tar\.gz)$ {
    expires 30d;
}

The stylesheet URL contains a hash of its content (style.css?v=1a2b3c4d), so it can be cached forever: when the file changes, the URL changes. HTML is not cached at all, which is fine, it is tiny.

Things I'd tell myself a year ago

  • try_files $uri $uri/ =404 plus directories with index.html give you clean URLs for free. No rewrite rules.
  • Turn on http2 and TLS 1.3 and forget about it. There is nothing to tune for a site this size.
  • Add a proper 404.html and error_page 404 /404.html;. The default nginx error page is ugly and tells everyone your exact nginx version.
  • server_tokens off; while you're at it.
  • A single-vCPU box with 2 GB of RAM serves static files faster than you'll ever need. The bottleneck is the uplink, not nginx.

That's it. Boring, which is the point.