Pencil Rolling
All writing

Notes on building a homelab

2 min read

I have been running a homelab since 2020. It started, as these things do, with a single Raspberry Pi and a misguided ambition to self-host Nextcloud. Six years later it spans three VPS instances across two providers, a private network stitched together with Tailscale, a Docker service stack behind Nginx, and a CI runner that builds my side projects while I sleep.

Most of what I know about computers, I learned by breaking this network and then fixing it.

The shape of the network

The topology is intentionally boring:

                    ┌──────────────────┐
                    │  Tailscale mesh  │
                    └────────┬─────────┘
        ┌────────────────────┼────────────────────┐
        ▼                    ▼                    ▼
   ┌─────────┐         ┌─────────┐         ┌─────────┐
   │  VPS-1  │◀───────▶│  VPS-2  │◀───────▶│  VPS-3  │
   │ (web)   │         │ (data)  │         │ (CI)    │
   └─────────┘         └─────────┘         └─────────┘

Each VPS runs a focused role. The web box fronts the public stuff — this site, a couple of side projects. The data box holds the databases and the longer-lived state. The CI box is disposable; if it dies, I rebuild it from a Dockerfile in twenty minutes.

Tailscale is the load-bearing wall. Before I adopted it, every service was a small networking puzzle: firewall rules, reverse tunnels, DNS hacks. After, every box was just on the same network. That single change probably saved me a hundred hours.

Lessons

A short list of things I would tell 2020 me:

  • Backups are a feature, not a chore. Treat them like one. Test the restore, not just the backup.
  • Boring technology is a competitive advantage — when the cost is your own free time, novelty is expensive.
  • Document the why, not the what. The what is in the code. The why evaporates in a week.
  • A status page is free accountability. Once you have one, you cannot pretend downtime does not count.
# docker-compose.yml — the part that runs the status page
services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    restart: unless-stopped
    volumes:
      - ./data:/app/data
    ports:
      - "127.0.0.1:3001:3001"

Why bother?

People occasionally ask why I run my own infrastructure when Vercel and Fly.io exist. The honest answer is: because I learn things. Latency, caching, failover, observability — these are abstractions on a managed platform and realities on your own. I do not want to be the engineer who has only ever seen them in a slide deck.

Also, it is fun. That is allowed to be a reason.