For years the homelab was a pile of half-remembered docker run commands. Pihole here, Jellyfin there, a Grafana that I'd started "just to try it" and never turned off. Every one of them had its own ports, its own restart policy if I'd remembered to set one, and its own way of dying quietly when the box rebooted. Bringing the lot back up after a power cut was an afternoon of archaeology.
So I did the boring thing and put everything in one Compose project. Not one giant docker-compose.yml, exactly, but one directory, one .env, and a stack I can bring up with a single command.
the layout
The structure is deliberately dull:
homelab/
.env
docker-compose.yml
pihole/
jellyfin/
grafana/
prometheus/
Each service gets a folder for its persistent data, bind-mounted rather than living in a named volume, because when something goes wrong at 23:00 I want to ls the config, not go spelunking in /var/lib/docker/volumes. The .env holds the handful of things that actually change between my setup and anyone else's: the timezone, the data root, the LAN subnet.
services:
pihole:
image: pihole/pihole:latest
env_file: .env
volumes:
- ./pihole/etc:/etc/pihole
restart: unless-stopped
restart: unless-stopped on every service. That one line is most of why the house now survives a reboot without me. The other half is that Compose brings things up in a predictable order and I no longer have to remember which container needs the database running first, because depends_on does it.
networks, not port soup
The thing that actually made this pleasant was stopping the port-mapping habit. Instead of exposing every service on a different high port and memorising which was which, internal services talk to each other over a Compose network by name. Only the things that genuinely face me, the reverse proxy and a couple of UIs, publish a port.
So Grafana reaches Prometheus at http://prometheus:9090, not some IP I'd have to keep current. When I move the whole lot to a different machine, nothing in the configs changes, because none of it was ever pinned to a host address.
what it bought me
The real win isn't elegance, it's that the state of the house is now a git repo. The Compose file and the configs are version-controlled, the data directories are excluded and backed up separately, and a fresh machine is git clone then docker compose up -d. I rebuilt the host onto a new SSD last weekend and the whole stack was back in about ten minutes, most of which was waiting for images to pull.
It is not clever. There's no orchestrator, no Kubernetes, no fleet management for four containers and a Raspberry Pi. But it's legible, and when something breaks I know exactly where to look. For a house, that's the whole point.