I wanted somewhere to keep my repositories and run CI that I control, that I can reach from anywhere, and that nobody else can reach at all.
This decision was hastened when GitHub was going down faster than a [dirty joke redacted] on a daily basis and was quickly becoming the weak link in my CI/CD pipeline.
So I built one, a mini PC on a shelf with two disks, Debian, and a stack of containers. I used nothing unusual, and I did that on purpose. In six months, I’m going to have to revisit these steps and I don’t want to get tripped up.
The Shape of It
The box runs Forgejo for git, a Forgejo Actions runner for CI, and a handful of small dashboards so I can see what it's doing without logging in. Every one of those is a container. The host underneath does almost nothing: it boots, mounts a disk, joins the network, and runs Docker.
That means I can replace the host. If the OS goes bad, I reinstall it, point it at the data disk, and start my stacks again, because nothing I care about lives on the system disk.
Private by Default
Nothing on this box is open to the internet. I have no router ports open, no public addresses, and no login page for strangers to try.
Every device I own joins a Tailscale network, and the box only answers on that network. My firewall drops all other traffic, including traffic from the home network it's plugged into.
It still has real HTTPS and proper names, like git.dev.tailnet.example. Those names are public DNS records that point at a private tailnet address. Anyone can look them up, but only my devices can get anywhere with the answer.
Two Disks, One Rule
The operating system gets one disk. The other one holds my compose files, the container data, the Docker image store, and the local copy of my backups.
My rule is that the system disk is disposable and the data disk is not, and most of the small decisions in the build follow from it.
The Parts That Fight
Installing each piece took me a few minutes. I spent my time in the places where two pieces meet.
Tailscale's own SSH breaks mosh. Containers can't reach Tailscale's DNS. A firewall that only trusts the tailnet also blocks a container trying to reach the box's own front door. Each vendor documents its own part, and the docs stop there.
I started calling those places seams, and they're most of what I'm writing about here.
I started calling those places seams, and they're most of what I'm writing about here.
How It's Written Up
Five posts tell the build in order. Alongside them, every component has a reference card with the vendor's standard steps, my changes and why I made them, the seams with the error you'll actually see, and how to check it worked. I wrote the posts as the story and the cards as the thing you'd keep open in another tab.
Metal and OS: the hardware, Debian, and the data disk.
Reachable From Anywhere, Visible to No One: Tailscale, the firewall, mosh and tmux (out in a few days).
Containers and a Front Door: Docker and Caddy (out in a few days).
Source Control and CI: Forgejo and its runner (out in a few days).
Watching It and Keeping It: dashboards, backups, alerts, and the reboot test (out in a few days).
All the cards are in one place, at /reference.1 I built the box to keep working when I stop paying attention to it. The reference is for when it gets cranky and I have to kick it once or twice.
No comments yet