The whole homelab is one small office mini PC running Ubuntu 24.04 and around 40 Docker containers. System and databases live on the internal SSD, media and bulk data on a big USB disk.
RAM is the real limit — it’s all used, and swap is full of cold pages. So the rule is: nothing heavy. I looked at self-hosting GitLab once; it alone would need half the machine. Code stays on GitHub.
How it’s organised
- One Compose stack per app in
/opt/docker/stacks, data in/opt/docker/appdata/<app>. - Arcane is the web UI for all of it — deploys, logs, image updates. One trap: Arcane injects
global variables at deploy time that live in its own database, not in any
.env. Rundocker compose upby hand and they silently resolve to empty strings. - Traefik in front of everything, with a wildcard Let’s Encrypt certificate via Cloudflare DNS.
Every app is
appname.mariusladner.dev— full names, no abbreviations, no exceptions. - AdGuard Home is the network’s DNS and DHCP server. It resolves
*.mariusladner.devto the server locally, and static leases get added through its API. - Remote access is Twingate for everything, plus a Cloudflare Tunnel for the few things that need to work without it.
- Glance is the dashboard: one screen, no scrolling. Service health, media queue, what’s coming up, recently added.
- Beszel for server stats, ntfy for push notifications, Cronmaster for cron jobs with logs I can actually read.
Maintenance that runs itself
- Daily updates at 6:05 — unattended
aptupgrade. When the upgrade includes Docker itself, the script restarts every container that mounts the Docker socket, because they’d otherwise keep a dead socket and silently stop seeing changes. - Daily: all Compose files and configs are pushed to a private GitHub repo.
- Weekly: appdata is mirrored to the USB disk.
- Image updates through Arcane. Lesson learned: when an app says “update available” but the
updater says “no change”, the tag is usually dead — Immich froze its
v2tag when v3 came out, and Jellyseerr moved to a new project (Seerr) while the old image just stopped getting builds.
What runs on it
Media
- Jellyfin for movies and shows, with hardware transcoding and tone mapping on the iGPU.
- Seerr to request things, Sonarr and Radarr to manage them, Prowlarr for indexers, SABnzbd to download, Recyclarr for quality profiles.
- UmlautAdaptarr so German titles with umlauts actually get matched.
- Bazarr for subtitles — English and German, full and forced.
- Watched movies are cleaned up automatically a few days after playing (favourites stay). Radarr unmonitors a movie once its file is gone, so it doesn’t get downloaded all over again.
Comics
Komga as the reader, Komf for metadata, Kapowarr for new issues. One folder per series,
named Series (year), files directly inside. Adding a comic is a Claude Code skill: “add the comic
in my downloads” and it lands in the right folder with the right permissions.
Everything else
- Immich — photos.
- Vaultwarden — passwords.
- Paperless-ngx — OCR’d documents, with mail intake from Gmail.
- Dawarich — my own location history instead of Google’s.
- Movary — movie diary.
- BentoPDF — PDF tools that don’t upload anything anywhere.
Home Assistant
- Five dashboards in one shared layout: three columns, each a heading, the main control, its settings, its automations, one graph.
- My gecko’s terrarium: the light follows sunrise and sunset, a rain system mists every evening after lights-off, and a watchdog kills the pump if it ever runs longer than three minutes — whatever turned it on.
- Dog walks: a GPS tracker on the dog plus our phones answers “how long was he out, and with whom” — whoever’s phone is closest to the tracker walked him. A nightly AI recap sums up the day.
- Fitness data from my tracker via the Google Health integration.
- Smart plugs, lights and sensors around the flat.
The WiFi problem
The server is on WiFi. The router’s band steering kept bouncing it between 2.4 and 5 GHz — hundreds of roams in a few days, downloads at a quarter speed. It’s now pinned to prefer 5 GHz with a fallback to 2.4, in a hand-written wpa_supplicant config (netplan can’t express two priorities for one network), plus a watchdog that restarts the connection if it drops. Since the server is also the network’s DNS, when it drops, the whole flat loses DNS.
Claude on the server
The server also runs Claude Code in Remote Control mode as a service. From my phone I can start a session on the server and ask it to check or fix something while I’m out with the dog. It has a strict rulebook: no network changes, no reboots, never touch the gecko’s watchdog.
What I’ve learned
- Verify behaviour, not config. Traefik ran a redirect rule for months that matched nothing,
while its dashboard showed it as enabled.
curlit. - Idle doesn’t mean unused. FlareSolverr logs nothing and looks dead — but Kapowarr needs it, and its Compose stack won’t even start without it.
- Measure usage from DNS. Traefik has no access logs here, but AdGuard’s query log shows which apps actually get used — once you filter out the dashboard’s own health checks.