Ramblings of an aging IT geek
← Ramblings of an aging IT geek
homelab

docker compose for the whole house

How I consolidated a sprawl of homelab containers into a handful of versioned Docker Compose files I can actually reason about.

Server rack with patch cabling

For a long time my homelab was a museum of how I'd felt about containers on any given weekend. Some services were docker run lines buried in shell history. A couple were lovingly hand-built with Portainer stacks I'd long since forgotten the shape of. One particularly cursed thing ran from a screen session that I was frankly afraid to detach. It all worked, in the sense that the lights were green, but I couldn't have told you how to rebuild any of it.

The fix wasn't clever. It was just Compose, applied with discipline. One directory per service, each with its own compose.yaml, all of it in a git repo I can clone onto a fresh box and bring up with one command.

one file per service, one source of truth

The rule I settled on: if it isn't in the repo, it doesn't exist. No more secrets living only in a container's environment, no more "oh that volume, I think it's somewhere under /opt". Each service gets a folder, a Compose file, and a .env that's gitignored but documented by a checked-in .env.example.

services:
  jellyfin:
    image: jellyfin/jellyfin:10.10.3
    container_name: jellyfin
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
    volumes:
      - ./config:/config
      - /tank/media:/media:ro
    ports:
      - "8096:8096"

The thing I'd been resisting for years, and shouldn't have, is pinning image tags. latest is how you discover at 23:00 on a Sunday that a database migrated its schema while you weren't looking. Pinning means upgrades are a deliberate act: bump the tag, read the changelog, docker compose up -d, watch the logs. Boring. Boring is the goal.

Homelab shelf with mixed hardware

the bits that make it bearable

A shared external network so containers can talk to the reverse proxy without me threading ports everywhere:

docker network create proxy

Then each service joins proxy as an external network and Caddy does the TLS and routing. The upshot is that adding a new service is genuinely a five-minute job: drop a folder in, add a label or two, up -d, done.

I also wrote a daft little wrapper that walks the tree and runs docker compose pull && up -d per directory, so a full update is one command without bundling everything into a single monstrous file. I tried the single-file approach early on and it became unmanageable around the tenth service. Separate files, shared network, shared conventions: that's the sweet spot.

It is not a fashionable setup. There's no Kubernetes, no GitOps operator reconciling state every thirty seconds. But I can read the whole thing in an afternoon, and when something breaks I know exactly which file to open. For a house, that's the right amount of infrastructure.