Homeprod Management with Docker Compose
Recently I decided to change how I manage my homeprod environment (home production, i.e. the things that other people in my household rely on and tell me if they're down). I moved everything over to docker-compose stacks managed with a small(ish) shell script. Skip down a bit if you don't want backstory.
Backstory
The situation before was a mish mash of various things that I've tried over the years. For awhile I was deploying everything with dokku on one big VM running on a proxmox host. That worked for some things but the 12 factor architecture doesn't always fit.
For those things I used Portainer to launch docker-compose stacks. Again, works for a lot but sometimes it's really annoying. Portainer's docker-compose support is somewhat limited in so far as you can't really ship configs along with a docker-compose file to be mounted into a container, so for anything custom you need to build your own derived container. Once you have that there are a lot of pointy-clicky steps to actually refreshing and deplolying an update.
The Fleet
I have a handful of systems running in homeprod:
hypnotoad
is the VM host. It's an HP Elitedesk 800 G3 mini running Proxmox 7netsvc1
is a Dell Wyse 3040 thin client running Technitium for DNS and DHCP with a nice web UI- There are 4 other Dell Wyse 3040 thin clients scattered around, one each in the house, office, garage, and shed. These have z-wave and zigbee USB sticks and are running zwavejs2mqtt and zigbee2mqtt.
All of the thin clients and most of the VMs are running Alpine Linux because the small runtime footprint meshes well with the 2gb of memory and 8gb of storage on the thin clients.
Every VM and physical node is connected to my Tailscale network as well.
Docker Compose Stack
The whole idea of this refactor is to centralize and simplify management without having to run SPOF orchestrators or heavy agents on the nodes (again: see the thin client specs above). I thought about a lot of things but nothing really clicked until one day while perusing a Hacker News thread I came across a tossed-off comment from someone who said they just used the offical docker compose container, bundled their stack into the container, passed in the docker socket and everything Just Worked.
This sounded like magic, so of course I had to try it. Of course it worked, but it's very limiting. It implies a single host per derived image, for one thing, and that doesn't work with my fleet.
The kernel of the idea was really great, though, so I iterated on it and came up with docker-compose-stack. Docker compose stack (terrible naming, sue me) is basically the same idea taken to a higher extreme.
How it works
Here's the workflow that docker-compose-stack runs:
- At startup,
start.sh
loads secrets from disk and, optionally, from a script nameddownload_secrets.sh
. It then runsrun_compose.sh
. - Run anything declared in
hosts.yml
for the current host as apre-start
script within the context of thedockerstack-root
container - Copy declared configs into
/var/lib/docker/stack_configs
- Creates a
.env
file from envirionment variables declared inhosts.yml
- Runs
sha256sum
over the contents of/var/lib/docker/stack_configs
and stuffs the result inCONFIGS_SHA
. - Composes a base
docker-compose
invocation from the list of stacks declared inhosts.yml
- Drops some cron jobs
- Runs
docker-compose
exec
s intocrond
to run the crons set up in step 7
Even without an entry in hosts.yml
, a host running docker-compose-stack
will always run a watchtower
container on a very short refresh cycle.
Watchtower will check every 30 seconds to see if any container images have been updated, download the update, and re-create each updated container.
That allows me to update my docker-compose-stack
container with GitHub Actions and every host just updates themselves accordingly.
That's it. That's the whole thing. I've been running my implementation of docker-compose-stack
across 12 VMs and physical nodes for about a week and it's been ticking along nicely.
Configs and Secrets
There's some nuance around secrets and configs that probably deserves some explanation.
Configs are exactly what you expect. Nginx configs, whatever. These get dropped into a directory on the host to be mapped in as container bind mounts.
Secrets are loaded from a special file on disk. Secrets can also be loaded from a script.
Loading works by running the script and capturing the output, evaling the output, and dropping the sha256sum
of the output into a file in the config directory.
The nuance comes in when we want to update secrets or configs on running containers. By including CONFIGS_SHA
in the list of environment variables for a service, it will automatically be recreated when that SHA changes. Otherwise changes aren't picked up very well.
In my homeprod environment I'm managing secrets using tailscale-op-proxy
which lets me tag a node with a Tailscale ACL tag and grab any secrets out of 1Password tagged with that same ACL tag. Nodes only get the secrets that they need and I get to manage secrets with the 1Password application rather than ssh'ing into each machine and managing them with vim.
Alternatives Considered
I looked at Kubernetes, but that was way too heavy. I also looked at Nomad which felt limiting, especially with regards to not wanting to run a SPOF orchestrator at all. I'm sure I could have implemented all of this with ansible or salt stack or puppet or whatever else.
I honestly just thought the docker-compose-within-docker idea was clever and decided to run with it.
Docker compose also has profiles
built in, but those are confusing because if you decide to remove a service from a node, if you're using profiles that service won't actually be removed because it's still part of the stack formation, according to the compose authors. That doesn't work for me, hence the machinations around building a compose command from a bunch of stack files.
You too?
I have esoteric tastes and don't really care about exploring production-grade infra systems like k8s in a home environment. My primary concern is that services stay up.
If that fits you too, and you run some stuff that other people rely on in your house, maybe take a look at docker-compose-stack
.