Hello, world

For a long time I kept meaning to put up a real website — somewhere the long things I write could live without being hostage to a platform's roadmap — and for a long time "somewhere" meant a rented VM I'd have to babysit. This post is about the version I finally built instead: a single binary running in a container on the NAS already sitting under my desk, reachable from the public internet through a tunnel. It took an evening, cost nothing a month, and the whole thing fits in my head.

Why bother

The honest reason is that I wanted the words and the pictures to be mine. Hosted platforms are convenient until they aren't — they change their ranking algorithm, they shutter, they decide your account is the problem1. A static file on a machine you control has none of those failure modes. The price is that you become responsible for the machine, which turns out to be a feature: it forces you to understand the thing you rely on.

The web was always meant to be a read/write medium for individuals. Reclaiming even a small piece of it feels less like nostalgia than like maintenance.

The shape of it

The site is one compiled Bun binary serving HTML it renders itself from Markdown on disk. No database, no CMS, no build step on the server — pages are parsed and cached by file mtime, so dropping a new .md into content/blog/ is enough to publish. The binary lives in an unprivileged LXC container on the NAS, with its content and uploads on a separate ZFS dataset bind-mounted in.

The path of a request

A request for, say, /blog/hello-world travels a surprisingly short distance:

# browser -> Cloudflare edge -> cloudflared (in the container) -> Bun :8080
GET /blog/hello-world
  Host: sieglinger.net
  -> Cloudflare nearest edge (anycast)
  -> cloudflared outbound tunnel (QUIC, no inbound ports)
  -> 127.0.0.1:8080  (the Bun server)
  -> render content/blog/hello-world.md from cache

The important property is that there are no open ports on my home network. cloudflared dials out to Cloudflare and holds the connection open; the public hostname is mapped to that tunnel, and traffic flows back through it. From the router's perspective nothing is being asked to listen.

Behind the tunnel

Cloudflare Tunnel is the piece that makes hosting from home feel safe. Because the connection originates inside my network, I never punch a hole in the firewall, and the public origin is never discoverable by scanning2. Cloudflare terminates TLS at the edge, applies whatever rate-limiting and bot handling I've configured, and forwards to my container over the encrypted tunnel. If I ever want to take the site down, I revoke the tunnel and it's gone from the public internet instantly — the data is still right there on the NAS.

What I gave up

This is not a setup that scales to a viral front page. The NAS has two cores earmarked for it and a gigabyte of memory, which is wildly more than a text-and-photo site needs but laughable by platform standards. If a post ever goes big, the tunnel or the box will sag before Cloudflare does, and that's fine — I'd rather a slow personal site than a fast one I don't control. Backups are also on me: the content is just files on ZFS, which snapshots cleanly, so I treat the dataset as the source of truth and content-seed/ in the repo as the bootstrap3.

What's next

The plumbing works; now the hard part, which is writing. I have a backlog of half-finished notes that deserve a permanent home, and a gallery's worth of photographs to put behind the shared passcode so family can browse them without me mailing attachments. If you're reading this on the day it went up, you're early — welcome, and watch this space.

Footnotes

  1. Not a hypothetical: I've watched entire archives vanish overnight when a service pivoted or shut down. Owning the bytes is the only durable fix.

  2. This also means the origin IP is never exposed, so the usual DDoS-to-the-IP play doesn't have a target. Cloudflare absorbs what it can; the tunnel simply isn't there if I turn it off.

  3. On first boot, the server copies anything in content-seed/ that isn't already present into the data directory — so a fresh machine serves real pages immediately without overwriting anything I've since edited.