A set of bench press I log at the gym on a Tuesday evening goes through four machines at home before the week is over. It lands on one, gets read by a second, its photo is filed on a third, and the app that recorded it was built on the fourth.

None of that was planned. A year ago this was one Intel NUC under my desk running a couple of containers. Each machine after that arrived to fix a specific annoyance, and each one got a name and a job. I'm writing the whole thing down, one post every Friday, using a single app as the thread that ties it together.

## The cast

Polaris is the hub. It's an Intel NUC 13 Pro with an i7-1360P and 64 GB of RAM, and it's the only machine that is expected to answer at three in the morning. It runs every API I have, the databases behind them, the internal dashboards, the CI runner, the private DNS, and the repository my phone installs my own apps from. If Polaris is down, things are down. I've written about how it grew from an LLM box into a server.

Andromeda is the AI machine, a DGX Spark I bought after deciding I shouldn't. It runs local models, image generation and anything in CI that needs a GPU. The important thing about Andromeda is what it's not responsible for: nothing anyone uses is allowed to break because it's busy, hot or asleep.

Boring-Drive is a Synology NAS on a shelf. The name is the spec. Storage should be the least interesting part of a homelab, and the day it becomes interesting is usually a bad day. It holds the things I can't regenerate: photos, archives, backups, the APKs my phone installs.

Hermes is the NUC 8 next to Polaris, and it's where I actually write code. It runs a full desktop with nobody sitting at it, and I reach it over remote desktop from my laptop or iPad. Android Studio, emulators and Gradle builds live there, so my laptop stays a thin client. It recently lost its Windows partition and gained a lot of disk.

## Deciding where something goes

Most of the design work was deciding which machine a new thing belongs on. I ended up with four questions, and I ask them in order:

Question If yes
Does it need to answer within a second, at any hour? Polaris
Does it need a GPU, and can it wait? Andromeda
If I lost it, would it be gone for good? Boring-Drive
Am I writing it, building it or testing it? Hermes

It sounds obvious written down. It wasn't obvious the first time I put a model server on the machine that serves APIs, or a database on a network share.

The four machines and my devices share one private network; the public internet reaches only Polaris, through a tunnel Polaris opens itself

## What ties them together

All four machines, plus my phone, laptop and iPad, are on one Tailscale network, a tailnet. Every device can reach every other one directly over an encrypted link, wherever it happens to be, and nothing outside the tailnet can reach any of them. My dashboards have names like real websites and valid HTTPS certificates, but those names only resolve for devices on the tailnet.

The public internet sees exactly one thing: a Cloudflare tunnel into Polaris that carries the handful of APIs that genuinely need to be public. Polaris opens that tunnel from the inside. That one decision gets its own post.

## The app I'll follow

Avrit is a gym tracker I built for myself. It has three parts: an Android app for logging workouts and body measurements, a Kotlin backend that holds the exercise catalog and a cloud backup, and a small web console where I manage members and the options the app offers.

Avrit's home screen: the week at a glance and the coach's suggestion for today

The screenshots in this series come from a demo account on a local copy of the server, filled with eight weeks of made-up training, so they show real screens without showing my real data.

I'm using it as the example because it touches everything:

  • The app logs sets with no network at all and syncs later, through the public tunnel, to the backend on Polaris.
  • The coach inside the app, which writes a weekly note and answers questions about my training, runs on a local model on Andromeda. When Andromeda is off, the app falls back to simpler rules and nobody notices.
  • The photo I take after a workout ends up as a plain file on Boring-Drive, in folders I can browse by year and month.
  • The app itself was written and built on Hermes, tested and signed by the CI runner on Polaris, and installed on my phone from a private F-Droid repository that only my own devices can see.
One workout's trip through the homelab, and the trip a new version of the app takes in the other direction

## The rest of the series

Each post takes one part of that picture and goes into how it actually works, what broke and what I'd do differently:

  1. This one: the map.
  2. The private side: Tailscale, private names on my own domain, and the dashboards behind them.
  3. The public side: APIs through a Cloudflare tunnel, and why the app never waits for them.
  4. Andromeda: a local model as a coach, and why it's only allowed to phrase numbers.
  5. Boring-Drive: what goes on the NAS, what doesn't, and the boot-order bugs it caused.
  6. Shipping: Hermes, the CI runners, the release process and a private F-Droid repository.

I'll keep the details at the level of how things fit together. Exact addresses, ports, config files and anything else that would help someone break in are left out, partly for obvious reasons and partly because they're the least interesting part. The shape is what took time to get right.

## A caveat before we start

This is three small computers under a desk and a NAS on a shelf. The "data center" is the shelf. Every piece of it was set up by one person, mostly in the evenings, and usually because something had annoyed me earlier that day. Some of it is held together by a cron job that checks whether something else came up properly after a reboot.

That's the part I wanted to write about. Not a reference architecture, just the one I actually run, with the reasons each piece ended up where it is.

Next Friday: the private side, and how a dashboard gets a real HTTPS name that the internet has never heard of.

support

Found this useful? You can buy me a coffee.

☕ buy me a coffee