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

one docker-compose file to run the whole house

Consolidating a sprawl of home services onto one box with a single docker-compose stack, the networking and volume decisions that made it maintainable, and the limits I hit.

A small home server rack with cables

My home services had grown the way these things do, by accretion. A Pi running one thing, an old laptop running another, a service I'd installed directly onto the NAS and then forgotten how. None of it was documented, all of it was load-bearing in some small domestic way, and when the laptop's disk finally started clicking I had no clean way to move what it ran. That was the push. I spent a weekend folding the lot into a single docker-compose.yml on one reasonably specced box, and six months later it's the homelab decision I'd most readily repeat.

The pitch for Compose at home is simple. The entire state of "what runs on this machine" lives in one file I can read, version, and back up. When I want to know what's running I read the file. When I want to move it all to new hardware I copy the file and the volumes. There's no longer a service that exists only as a thing I did once at a shell prompt and can't reconstruct.

one file, sensible sections

The file itself is unremarkable, which is the point. Each service is a dozen lines: image, restart policy, the ports it needs, the volumes it owns, and a couple of environment variables. A trimmed example:

version: "3.7"

services:
  reverse-proxy:
    image: traefik:1.6
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik:/etc/traefik

  dns:
    image: pihole/pihole:latest
    restart: unless-stopped
    ports:
      - "53:53/tcp"
      - "53:53/udp"
    volumes:
      - ./pihole/etc:/etc/pihole
    environment:
      - TZ=Europe/London

  git:
    image: gitea/gitea:latest
    restart: unless-stopped
    volumes:
      - ./gitea:/data

restart: unless-stopped on everything means the stack survives a reboot without me, which matters because the box lives in a cupboard and I'd like to think about it as rarely as possible. The volumes are all bind mounts under the project directory rather than named volumes, deliberately, so that "back up the homelab" is just "tar one directory". Named volumes are tidier but they hide where your data lives, and at home I'd rather see it.

A homelab setup with services and dashboards

the networking was the interesting bit

The first naive version exposed a port for every service and I navigated to things by remembering port numbers, which is exactly as pleasant as it sounds. The fix was a reverse proxy out front, Traefik in this case, mostly because it can read labels off the running containers and configure itself. I add a few labels to a service and it gets a hostname, no separate config file to keep in sync with reality:

  git:
    image: gitea/gitea:latest
    restart: unless-stopped
    labels:
      - "traefik.frontend.rule=Host:git.home.lan"
    volumes:
      - ./gitea:/data

Now everything answers on a friendly name on my LAN. Pi-hole resolves *.home.lan to the box, Traefik routes by hostname, and I never type a port number again. That combination, local DNS plus a labelling reverse proxy, is what took the setup from "a pile of containers" to something that feels like a small private platform.

The one genuine wrinkle was DNS and the proxy depending on each other in a cycle: Pi-hole resolves the names, but it's also a service behind the same box. I keep Pi-hole on a fixed host port and point the router's DNS straight at the box's IP rather than a hostname, so resolution never depends on resolution. It's the sort of bootstrapping problem you only notice the first time the whole thing comes up cold after a power cut.

where it stops being enough

I'll be honest about the limits, because the "everything in one Compose file" posts rarely are. This is one machine. If it dies, the house goes quiet until I fix it, and a single docker-compose.yml is a single point of failure dressed up as tidiness. For a home that's an acceptable trade: the blast radius is me being mildly inconvenienced, not a business. But I wouldn't pretend it's high availability, and I don't run anything here that anyone but my household depends on.

Compose also has nothing to say about updates beyond pull and up -d, which has bitten me when a :latest tag moved under me and a service came back subtly broken. I've started pinning the images that matter to real version tags and only floating :latest on the ones I don't mind babysitting. That's a small discipline that turns "why did DNS stop working overnight" into a non-event.

The headline result is the one I wanted: the whole house now runs from a file I understand, on hardware I can replace in an afternoon. When I inevitably outgrow one box, the file is also the migration plan. That's worth a weekend of folding things together, and it's worth the small ongoing discipline of keeping it honest.