On this page
You took part one’s promise seriously, and now you have a small fleet of Docker Compose stacks — and a new, quieter problem nobody writes tutorials about. Every one of those services is aging: updates sit unapplied because you’re not sure what docker compose pull might break, your “backup plan” is a vague sense that the data is in there somewhere, and moving a stack to a newer machine feels like open-heart surgery. This is the maintenance manual I wish part one could have ended with — update without fear, back up what you can’t re-download, and move a whole stack like it’s just files. Because it is.
A stack is three things, not one
Every bit of maintenance confidence comes from one mental picture. A running stack is three different kinds of stuff, and they have completely different replacement costs:
The recipe — your compose.yaml and .env — is two small text files you wrote. The images are big, but they’re a cache: the registry — the online image library you pulled them from, Docker Hub for most of us — will hand them back anytime. The data — named volumes and bind mounts — is the only part of the whole arrangement that exists nowhere else on Earth. Once that picture clicks, everything in this post becomes obvious: updating swaps the middle column, backing up saves the outer two, and migrating moves the outer two while the middle re-downloads itself.
I’ll use the same shape of example our Uptime Kuma post deploys — Uptime Kuma itself, a single service with one named volume — because it’s the shape most homelab stacks share.
The examples below use placeholder values — swap them for yours before copying anything:
/opt/stacks/uptime-kuma— my example stack folder. Use wherever your compose file actually lives.kuma-data/uptime-kuma_kuma-data— an example volume’s short name and its full on-disk name (Compose prefixes the project name).~/stack-backups— an example backup destination. Anywhere with space works; ideally a different disk.10.0.0.20andyouruser— the new host and user in the migration example.
Rule of thumb: if a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.
Task 1: The update ritual
Here’s the thing that surprises everyone: docker compose up doesn’t update anything by itself. Per Docker’s own reference, up stops and recreates containers only when the service’s image or configuration changed after the container was created — and crucially, it does that preserving mounted volumes. No pull, no update. So updating is a deliberate two-step, and I’ve learned to wrap it in a small ritual:
Before touching anything, glance at the project’s releases page for the services in this stack. You’re scanning for one phrase: breaking change. Minor versions are almost always safe; major versions of anything with a database deserve real attention — PostgreSQL, for example, requires an explicit migration step between major releases, and a container happily starting a new major version against an old data directory is how people lose weekends. This is also why I pin images to a major tag — the version label after the colon — like uptime-kuma:2 instead of latest: updates stay inside a lane I chose, and crossing into the next lane becomes a decision instead of a surprise.
An update you can roll back is boring, and boring is the goal. Task 2 below makes this a one-script habit — run it now, before the pull. The worst time to design a backup process is while something is broken.
cd /opt/stacks/uptime-kuma
docker compose pull # fetch newer images (nothing restarts yet)
docker compose up -d # recreate ONLY what changed, volumes preserved
The nice property of this pair: pull is completely safe — it just downloads. Nothing about your running stack changes until up -d, and even then Compose only touches services whose image actually moved. A three-service stack where one image updated recreates one container.
Open the service, click around, check the logs (docker compose logs --tail 50). The old image versions are still on disk doing nothing, and how you clear them depends on how you pin. If you follow a rolling tag like uptime-kuma:2, the superseded image is left untagged — “dangling” — and docker image prune clears it. If you updated by editing a pinned tag, say nginx:1.27 to nginx:1.28, the old image keeps its tag and plain prune won’t touch it; remove it with docker rmi nginx:1.27 once you’re sure. Either way I wait a day or two first: the old image is your instant rollback — point the compose file back at it and up -d again.
docker compose down removes containers and networks but — per the down reference — leaves named volumes alone. Adding -v changes that: it deletes the named volumes too. There is exactly one situation where you want -v (deliberately erasing a stack forever), and zero situations where you want to discover the difference by accident. Treat down -v the way you treat rm -rf.
The Watchtower question
Every Compose maintenance conversation eventually arrives at Watchtower, the tool that watched your containers and auto-updated them when new images appeared. So let me save you some outdated advice: the original Watchtower is gone. The containrrr/watchtower repository was archived by its owner on December 17, 2025 — it’s read-only now, receiving no updates and no security patches. The maintainers were candid: they’d stopped using Docker heavily themselves and lost the time and interest to keep it alive. They deliberately declined to crown a successor, and warned users to evaluate any fork’s code and history themselves before trusting it.
There is an actively maintained fork — nicholas-fedor/watchtower — and if you’re set on auto-updates it’s the one to evaluate. But my honest recommendation for a homelab hasn’t changed, and the archive only strengthened it: auto-updating stateful services — anything that keeps data, which in a homelab is nearly everything — was always a gamble. A tool that pulls a new major database version at 3 a.m. and recreates the container doesn’t read release notes first. The update ritual above takes five minutes; run it on a schedule you choose — a monthly “patch morning” with coffee works — and the day something breaks, you’re standing right there with a fresh backup, not asleep.
Task 2: Back up the irreplaceable third
Remember the picture: the recipe and the data are the only parts worth saving. The recipe is trivial — copy two text files. The data needs one trick, adapted from the throwaway-container approach in Docker’s own volume documentation: rather than digging around in Docker’s internal storage, you run a throwaway container that mounts the volume read-only next to a backup folder, and tars one into the other:
cd /opt/stacks/uptime-kuma
docker compose stop # optional but safest — a quiet copy is a clean copy
docker run --rm \
-v uptime-kuma_kuma-data:/source:ro \
-v ~/stack-backups:/backup \
busybox tar czf /backup/volume-kuma-data.tgz -C /source .
docker compose start
cp compose.yaml .env ~/stack-backups/ # the recipe rides along
Two details that matter. First, the volume’s full name is the project name plus the short name — the folder uptime-kuma plus the volume kuma-data gives uptime-kuma_kuma-data; docker compose config --volumes lists the short names if you’re unsure. Second, the stop matters most for databases: tar-ing a volume while a database writes to it can capture a mid-write mess that restores into corruption. For anything with a real database, stop the stack for the seconds the tar takes — or better, use the app’s own dump tool (pg_dump and friends) and back that up.
I’ve wrapped all of this — every volume in a stack, the recipe files, timestamps, an optional stop/start — into the Compose stack backup playbook, so a nightly cron job (Linux’s built-in scheduler) can do the worrying.
If your Docker host is a container or VM on Proxmox, then Proxmox Backup Server is already snapshotting the entire host — volumes, images, compose files, all of it. That’s your disaster-recovery layer. The per-stack tar backups here are the surgical layer: restore one service’s data without rolling back a whole machine, or carry one stack to a different host. You want both, and neither counts until you’ve watched a restore actually work.
Task 3: Move a stack to a new host
Sooner or later a stack outgrows its host, or the host gets rebuilt, and here the three-things picture pays off completely: migration is just Task 2’s backup, restored somewhere else. The images never travel — the new host pulls them fresh from the registry.
rsync -av /opt/stacks/uptime-kuma youruser@10.0.0.20:/opt/stacks/
rsync -av ~/stack-backups/volume-kuma-data.tgz youruser@10.0.0.20:~/
rsync — a copy tool that runs over SSH and only sends what changed — carries the stack folder and the volume tar to the new machine; plain scp works too. Keep the same folder name on the new host. Compose derives the project name from the directory name by default (project name docs), and the project name prefixes every container, network, and volume. I’ve run a stack from a differently named copy of its folder and stared, baffled, at duplicate containers — same compose file, different project, so Compose happily built the whole stack twice. Same name, no surprises.
cd /opt/stacks/uptime-kuma
docker volume create uptime-kuma_kuma-data
docker run --rm \
-v uptime-kuma_kuma-data:/target \
-v ~/:/backup:ro \
busybox tar xzf /backup/volume-kuma-data.tgz -C /target
docker compose up -d # pulls images fresh, finds its data waiting
Creating the volume with its full prefixed name before up matters — that way Compose adopts the volume that already has your data in it, instead of creating an empty one. Expect a warning that the volume “already exists but was not created by Docker Compose” — that’s the adoption working, not a failure; Compose reuses the volume untouched.
The tar handles the volumes; these are the things that don’t travel automatically:
- Bind mounts: a
./confignext to the compose file came along with the folder, but an absolute path like/mnt/mediahas to exist — with your data in it — on the new host too. Docker silently creates a missing host folder as empty, so the failure isn’t an error message; it’s a service that comes up blank. - Ownership: Linux stores file owners as numbers, not names, and the tar preserves those numbers — if the service ran as a specific user, make sure that number means the same account on the new machine.
- CPU architecture: images are built per chip family. Moving from an x86 box to a Raspberry Pi means every image in the stack needs an ARM build to exist (most popular ones ship both).
- Ports and devices: anything in the compose file referencing a host device or an already-taken port needs a once-over.
Then point your bookmarks, reverse proxy, or monitoring at the new address, watch it go green, and retire the old copy after a few quiet days — not before.
What’s next
Your stacks now update on your schedule, survive their own deaths, and aren’t married to their hardware. If you want more practice on a meatier example, the arr stack deployment is a multi-service Compose build these exact habits keep healthy — and the localhost trap post covers the networking mistake most likely to bite when services move.
One last nudge: every path, name, and address above was an example. If a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.
Maintenance is where the destructive commands live. Paste one into Explain Before You Run and it will tell you what it touches, rate it red/amber/green, and answer the question that matters — whether you can undo it. Worth doing with any prune command in particular.
Related posts:
- Your First Docker Compose Stack — part one: images, containers, ports, and volumes from zero
- Docker on Proxmox: LXC vs VM — the host these stacks live on
- Deploy the Arr Stack with Docker Compose — a real multi-service stack to practice maintenance on
- Proxmox Backup Server — the whole-host backup layer under your per-stack tars
- Measure Your Proxmox Restore RTO (Honestly) — a backup you haven’t restored is a hope, not a plan
- The Docker localhost Trap — the container-networking gotcha that strikes after migrations
- Uptime Kuma: Dead-Simple Homelab Monitoring — the example service, and the thing that tells you the migration worked
Comments
Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.