The workhorse. Dashboard, inventory, bookmarks, uptime monitoring, Paperless-ngx, OpenProject, Collabora, whiteboard, task tracking, offline Wikipedia, and the monitoring UI for every other node.
A seven-machine homelab that runs a family's software
Five machines form a Docker Swarm. Two stand alone on purpose. Together they run more than thirty self-hosted services for photos, files, media, documents and DNS, built to be reliable and rebuildable from backups and a documented procedure.
Each box has one job it is good at
Nextcloud on a custom-built image, with MariaDB and Redis behind a host-side nginx. The family's files, calendars and sync.
Photo and video library with face recognition and semantic search. The machine-learning jobs get a full 8-thread CPU, and the photos live on network storage, so this node can be rebuilt without touching them.
The utility drawer: PDF tools, developer utilities, local file sharing and budgeting. Small, stateless apps that suit a small machine.
Network-wide DNS filtering with Pi-hole for every VLAN, plus a monitoring agent. It is also a Swarm manager.
Jellyfin for video, Navidrome for music, and a Sonos bridge. Jellyfin is installed on the host itself; the rest runs in Docker Compose.
A separate Nextcloud for a separate organization, kept out of the family cluster.
Where the setup deliberately goes two ways
Most of the architecture comes down to a handful of either/or choices. Here is each one, what sits on each side, and the reason for the split.
Swarm vs standalone
Swarm Five nodes
- One
deploy.shper stack, same shape everywhere - Node labels decide what runs where, so a service finds the right machine on its own
- Overlay networks link stacks across machines
Standalone Two hosts
- Plain Docker Compose, managed on the host
- Jellyfin left the Swarm. It is installed on the host itself, with its music server, Sonos bridge and tunnel in plain Compose beside it
- The second Nextcloud belongs to a separate organization, so it stays outside the family cluster
The rule: use the cluster where scheduling helps, and stay out of it where it would only add moving parts.
Big x86 boxes vs small Pis
x86 office and immich
- Anything memory- or CPU-hungry: OpenProject, Paperless and document editing on office, photo machine learning on immich
- The two 32 GB machines
ARM Raspberry Pis
- Single-purpose services with a known footprint: Nextcloud, Pi-hole, small utilities
- Cheap, quiet and low-power, and easy to replace from a runbook
What changed it: OpenProject, Collabora and the whiteboard once ran on a Pi 4 that kept running out of memory. Moving them to the x86 box fixed it.
Public vs private access
Public A short list
- Nextcloud, OpenProject, the Sonos bridge, and Home Assistant
- Reached through Cloudflare tunnels, except Home Assistant, which uses its vendor's cloud relay
Private Everything else
- Reachable only over Tailscale, with HTTPS from Tailscale's own certificates
- Tailscale runs on the host, not in a container, which avoids Swarm networking quirks and keeps it working when the cluster does not
The rule: a service is private until there is a reason for a family member to open it from anywhere.
On the host vs in a container
Host Foundation
- Tailscale, monitoring agents, Jellyfin, and the backup timers
- These need to work when Docker does not, and they are what you use to fix it
Container Applications
- Every application, defined in version control and redeployable in minutes
- Pinned where a version matters, such as the search engine behind the bookmark manager
The rule: anything the cluster depends on stays outside it.
What I changed, and what each change fixed
Backups first, then everything else
- Backups run from systemd timers, staggered across nodes so monitoring stays available during each window.
- A restore always goes to a scratch location first. It is verified there before it touches production.
- Passphrases and keys sit in a password manager, and are rotated only after a suspected exposure or a change in who has access.
- Ansible configures every host, so a rebuilt Pi gets Docker, Tailscale, storage mounts and backup units from one command. I preview each run first with check and diff.
- Rebuild-from-nothing runbooks exist for each node, kept next to the code.
- The whole setup lives in one Git repository, with secrets kept out of it.
Things the cluster taught me
Swarm ignores env_file
It does so silently. Declare every variable in the service's environment block and load the env file into the shell before deploying.
Labels vanish on reboot
Placement constraints depend on node labels, so one script reapplies them all. "No suitable node" almost always means a missing label.
Keep SQLite off NFS
Query timeouts appear under load. Anything backed by SQLite gets local disk.
Root squash blocks containers
With root squash on, a container writing as root fails without an error message. Map root on the NAS export before debugging the app.
Pin what can corrupt data
The bookmark manager's search engine stays on one exact version, because an upgrade breaks its index.
Stale overlay state looks like many failures
When several services error out at once with "no such container", restart Docker on that node before chasing individual apps.
On the list
- A local language model on the x86 node to power document search and drafting in Nextcloud and Home Assistant.
- Finishing the Ubuntu 26.04 LTS rollout across the remaining machines.
- Ansible Vault for the remaining secrets, and a validation play for adding new nodes.