thea

self-hosted homelab

a linux server running my own cloud (files, media, notes, passwords), rebuildable from a runbook.

jun 2025 · infrastructure

linux · docker · cloudflare tunnel · tailscale · postgresql · minio · nextcloud · vaultwarden · sops/age

active, running continuously and fully documented.

A self-hosted Linux server that quietly runs the parts of my digital life I used to rent (file sync, my notes, a media and books library, a password manager), all on one old laptop I never intend to turn off. It reaches the internet through a Cloudflare Tunnel instead of open ports, keeps its secrets encrypted, and, more than any single service, it's reproducible. I can rebuild the whole machine from a runbook I wrote as I went. There's something quietly satisfying about owning the thing your whole digital life runs on, even, maybe especially, when it breaks.

why i built it

It started with something small and a little petty. I wanted to sync my Obsidian vault across my devices without paying the subscription fee. That one annoyance sent me looking for alternatives, and I stumbled into self-hosting, a WebDAV server of my own, with MinIO buckets holding all my notes. I had a spare laptop lying around doing nothing, so I turned it into a server, got the vault syncing, and then just… didn't stop.

Obsidian sync led to Pi-hole to kill ads across the network, which led to Nextcloud for files, which led to Tailscale so I could reach any of it from anywhere. Somewhere in there I fell down the rabbit hole of registering my own domain and carving out subdomains for each service, so instead of memorising ports I could visit drive. or books. and have it just load, from my phone, on cellular, with everything still under my control. What began as dodging one subscription slowly turned into buying back ownership of my whole setup, one service at a time.

the notes sync path, goodnotes and the obsidian vault flowing through rclone and minio buckets out to nextcloud and drive

Underneath it all was privacy and control (I liked knowing the data lived on hardware I could physically touch) and, honestly, curiosity. I wanted to actually understand Linux, Docker, and networking, and there's no faster way to learn than running real infrastructure you depend on. And because the whole thing grew out of my frustration with subscription fees, I wanted to document it well enough that anyone with the same itch could rebuild it and skip the fees too. The server earns its keep in quieter ways too. It's where my Spotify history-collecting pipeline runs on a schedule, night after night.

the thinking

A few decisions shaped how it grew up. I put everything behind a Cloudflare Tunnel instead of open ports, partly because my ISP sits behind CGNAT and inbound connections simply don't arrive, but mostly because I liked that nothing is left listening on the public internet at all. Tailscale handles the rest when I actually need to administer the box. Secrets go through SOPS + age, so the encrypted store can live right inside the vault and sync freely, and plaintext only ever exists for the moment I decrypt it.

But the decision I care about most wasn't technical. Early on I realised a homelab you can't rebuild isn't an asset, it's a fragile single point of failure you've grown to depend on. So I started treating every break as a documentation event. Whatever I did to fix it went straight into a rebuild runbook. It changed how the whole thing felt. A service going down stopped being a small panic and became a procedure I could follow, and that shift, from memory to procedure, is the part of this project I'm quietly proudest of.

the service topology, native systemd services, docker stacks, and the tunnel and tailscale access paths

how it works

Three layers coexist on the metal. Native systemd services own the fundamentals, Pi-hole for DNS and adblock, Samba for file shares, PostgreSQL, cloudflared for the tunnel, Tailscale for remote access, and a few rclone WebDAV mounts. Docker runs the applications, Nextcloud (files/drive), MinIO (S3-compatible object storage that backs the Obsidian sync), Vaultwarden (passwords), and a books library served over WebDAV, all managed through CasaOS and Portainer. Everything user-facing is reached by a public hostname mapped through the tunnel's ingress rules (drive. → Nextcloud, obsidian. → MinIO, and so on), so there's a clean split between what runs and how it's reached. Full raw configs live in the repo, and this page is the narrated version.

walkthrough

Reproducible from bare metal. Each stage is the goal, the shape of the work, and the sharp edge I hit.

1. the host

An old ThinkPad T480 acting as an always-on server, Ubuntu 24.04 LTS, 16 GB RAM, single SSD. Gotcha. it's a laptop, so set "do nothing on lid close" or it suspends the moment you shut it. A DHCP reservation pins its LAN IP so the tunnel and shares always find it.

2. networking and access

Pi-hole becomes the LAN resolver, and UFW closes everything that isn't needed. Tailscale is the primary remote path, tailscale up --advertise-exit-node --advertise-routes=192.168.1.0/24 with ip_forward enabled, because it works through CGNAT with no port-forward. Gotcha learned the hard way. fail2ban's sshd jail bans your own IP after a few failed logins during setup.

3. the tunnel

Install cloudflared as a native service and define ingress in /etc/cloudflared/config.yml, one hostname per local target. The gotcha that cost me time. I also had a cloudflared Docker container running the same tunnel, two runners fighting over one connection, crash-looping. Keep exactly one, and I kept the native service.

4. services and storage

Bring up the Docker stacks (Nextcloud, MinIO, Vaultwarden, books WebDAV) and wire the Obsidian vault to sync against a MinIO bucket. Storage is a single disk with no redundancy, so off-box backups are the only safety net, a constraint written down rather than wished away.

5. secrets

Real values live in a SOPS-encrypted *.sops.yaml (AES-256 via age), safe to sync into the vault. Decrypt with sops -d using the age key at ~/.config/sops/age/keys.txt, which exists only on the Mac, so backing up that one key is the real disaster-recovery dependency.

verify

You know it's up when dig @192.168.1.87 resolves through Pi-hole, the tunnel hostnames load their apps from off the LAN (proving no port-forward is involved), Tailscale reaches the box over cellular, and sops -d round-trips the secret store.

what i learned

The biggest surprise was how little of "infrastructure" is actually the services, those were the easy, fun part. The real work, and the real value, was the bookkeeping. Writing down the storage layout, the tunnel config, the backup steps, and every sharp edge as I hit it, so that a future me (probably tired, probably at a bad time) could rebuild the machine without having to remember anything. It taught me that documentation isn't the boring chore after the project. For something you actually depend on, it is the project.

It's not perfect, and I try to be honest about that. It's a single aging disk with no redundancy, so off-box backups are the only real safety net, and there are a couple of half-finished cleanups I've flagged and not gotten to. But I can genuinely rebuild the whole thing from my own notes, and it's been quietly running my digital life for long enough that I mostly forget it's there, which, for a server, is the highest praise I've got.