Overview
One bare-metal Debian machine. Docker Compose stacks for everything container-shaped, native systemd services for the long-running things, Nginx in front of the public stuff, Tailscale in front of the private stuff. No Kubernetes. No orchestration layer. Just compose files in a git repo and a few well-placed systemd timers.
This page is a current inventory — what's actually running as of the last deploy, not aspirational architecture. I update the breakdown when I add a service, drop one, or kill something that didn't earn its keep.
The host
Topology
Each Docker stack has its own docker-compose.yml, pinned image versions, named volumes for state, and a restart: unless-stopped policy. Compose files are version-controlled in GitHub so I can rebuild any stack from a fresh host in under 10 minutes.
Service inventory
| service | role | image / source |
|---|---|---|
| Navidrome | self-hosted music (Subsonic API) | navidrome/navidrome |
| Audiobookshelf | audiobooks & podcasts | audiobookshelf/audiobookshelf |
| Sonarr | TV show automation | linuxserver/sonarr |
| Radarr | movie automation | linuxserver/radarr |
| Prowlarr | indexer manager for *arr | linuxserver/prowlarr |
| qBittorrent | torrent client (LAN only) | linuxserver/qbittorrent |
| Immich | photo & video backup (Google Photos replacement) | immich-server + pgvecto-rs + valkey |
| service | role | image / source |
|---|---|---|
| Pi-hole | network-wide ad blocking DNS | pihole/pihole |
| dnsmasq | DHCP + local DNS caching | systemd (host) |
| avahi | mDNS / Bonjour for LAN discovery | systemd (host) + docker |
| Tailscale | WireGuard mesh + Funnel for public ingress | tailscaled (host) + docker |
| Tor relay | contribute bandwidth to the Tor network | tor@default (host) |
| Samba | SMB/NFS file shares to LAN devices | smbd / nmbd (host) |
| service | role | image / source |
|---|---|---|
| Nginx | reverse proxy, SSL termination, vhost routing | nginx (host) |
| Postfix + Dovecot | self-hosted mail (IMAP/Submission) | postfix + dovecot2 (host) |
| Netdata | per-second system telemetry | netdata (host) |
| Homepage | service dashboard (read-only status) | gethomepage/homepage (docker) |
| Uptime Kuma | HTTP / TCP / DNS uptime checks + alerts | louislam/uptime-kuma (docker) |
| NetAlertX | LAN presence detection (who's home, by MAC) | netalertx/netalertx (docker) |
| Home Assistant | home automation hub (configured, run on demand) | homeassistant/home-assistant |
| service | role | image / source |
|---|---|---|
| ai-core | local LLM helper for batch jobs | Python venv (host) |
| super-hub | LAN device orchestration & control plane | Python venv (host) |
| tailor-tool | Flask app that tailors a resume to a JD | Python venv (host) |
| tv-bridge | convert Google Nest commands to Roku/FireTV IR | Python systemd (host) |
| job-watcher | Inbox watcher that triggers the resume pipeline | Python systemd (host) |
How I expose things
Three ingress layers, used for different jobs:
- Tailscale is the default. Anything I want to reach from my phone, my laptop on coffee-shop wifi, or my car is behind the tailnet. No public ports for admin interfaces.
- Tailscale Funnel exposes one service publicly (the portfolio's /api/uptime health endpoint) without punching a hole in the home router.
- AWS hosts everything recruiter-facing. Portfolio site, contact form, resume PDF, project catalog, sitemap, robots.txt — all on S3 + CloudFront + Lambda. The homelab doesn't serve web traffic directly.
Backups
Three layers, in order of how painful it would be to lose:
- Compose files — git, on GitHub, public. Recovery is git clone && docker compose up -d.
- Immich + Navidrome + Audiobookshelf state — nightly rsync from named volumes to a separate disk on the host, 7-day retention, a weekly rsync off-host to a friend's box in another state.
- Mail spool + Samba shares — weekly tar.gz to the same backup disk. Mail is recoverable from Gmail IMAP if I lose more than a week of inbound.
Things I actually learned
Self-hosting mail is a rite of passage
Postfix + Dovecot + DKIM + DMARC + reverse DNS + IP reputation. I expected it to be one weekend. It was three weekends, a support ticket to my ISP asking them to set PTR records, and four rounds of "why is Gmail marking this as spam." I still self-host because I want the muscle memory for SMTP, but I also now understand why almost nobody does.
Pi-hole in a container behind Tailscale works surprisingly well
The whole network's ad-blocking DNS now resolves through a container running on the host, with Tailscale as the only ingress for the admin UI. Means I can toggle blocklists from my phone without ever opening a port on the home router.
Compose volumes should match what you actually want to lose
First Immich rebuild, I forgot to mount the named volume for thumbnails onto the new container. Six months of thumbnails regenerated, but the originals were safe because they were on a separate volume. Naming matters. immich_data ≠ immich_thumbs ≠ immich_postgres.
Monitoring you don't look at is not monitoring
Uptime Kuma sits on the dashboard. I check it daily. Netdata is too dense to stare at — I only look at it when something feels off, but the historical graphs have saved me at least twice when "feels off" turned into "was actually off for 40 minutes overnight."
The boring infra is what fails the loudest
Most outages I've had weren't an app crashing. They were a full /var partition (syslog not rotated), a stale Docker bridge, or a cert that the Let's Encrypt wild-card DNS challenge couldn't renew because dnsmasq was answering NXDOMAIN before the upstream. The fix is always unromantic: log rotation, journalctl --vacuum-size, cron monitor.
Caveats
- Single host = single point of failure. I'm comfortable with that for personal services; I wouldn't ship it for a business.
- The mail server is operational, not high-availability. Gmail and Fastmail remain my primary inboxes.
- The Tor relay shares the host's bandwidth. It's capped at a few hundred KBit/s so I don't notice it.
- Nothing in this list is doing anything sketchy. Immich is on a LAN-only IP. qBittorrent is LAN-only. The *arr suite automates downloading things I already own or buy.
What's next
Two specific things, both small:
- K3s on a second box, so I can move the high-availability pieces (mail, Samba) off a single host. Not a full cluster — just enough so that one dead box doesn't mean no mail.
- Replace the cron rsyncs with restic, encrypted, pushed to Backblaze B2. Rsync is fine. Restic is fine-plus.