For about a year my homelab has been a collection of docker run commands held together by shell history and optimism. Each service started with a different invocation, half of them I'd forgotten the flags for, and rebooting the box meant a tense ten minutes of remembering what was supposed to be running and in what order. This weekend I finally moved the whole lot into a single docker-compose.yml, and it's the first time the lab has felt like a system rather than a pile.
The win isn't clever, it's just that everything is now written down in one place. The file is the documentation. If I want to know what's running, I read it. If I want to change a port or a volume, I edit it and run one command.
version: '2'
services:
proxy:
image: jwilder/nginx-proxy
ports:
- "80:80"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
unifi:
image: linuxserver/unifi
restart: unless-stopped
volumes:
- ./unifi:/config
environment:
- VIRTUAL_HOST=unifi.home.local
A few things made the difference between "works" and "pleasant to live with".
First, restart: unless-stopped on everything that matters. The old setup didn't survive a reboot without me babysitting it. Now the box comes back up, Docker starts, Compose brings the stack back, and the services return on their own. The one caveat I hit: unless-stopped remembers if you deliberately stopped a container, which is what you want, but it means a container you stopped on purpose won't quietly come back after a reboot. Took me a confused minute to remember I'd stopped it myself.
Second, named volumes and bind mounts for anything stateful, kept out of the container. Compose makes it tempting to treat containers as permanent, but the whole point is that the container is disposable and the data isn't. Config under ./service/config, all of it in the same directory as the compose file, all of it in a git repo. Blowing away a container and pulling a fresh image is now a non-event because the state never lived in the container to begin with.
Third, the nginx-proxy container that watches the Docker socket and routes by hostname. I set VIRTUAL_HOST on each service and it builds the reverse proxy config automatically. No more remembering which service is on which port. unifi.home.local, grafana.home.local, done. It's a small piece of magic but it removed an entire category of "wait, was that 8080 or 8443" from my life.
The honest catch: a single compose file is a single thing to break. Get the YAML indentation wrong and nothing starts, and YAML's whitespace will absolutely punish a stray space. I keep the file in git now partly so I can see exactly what I changed when the stack won't come up. But that tradeoff is fine. I'd much rather debug one declarative file I can read top to bottom than reconstruct a year of forgotten docker run flags from memory at the exact moment I least want to.
docker-compose up -d. The whole house, one command. I should have done this months ago.