Updating Self-Hosted Apps Without Fear
Published 2026-06-23 · Updated 2026-07-31 · 20 min read · By Ben Liu
A repeatable update ritual: changelog, snapshot, migrate, verify, and roll back — with lab notes from upgrade nights.
Read before you pull
Skim changelogs for breaking changes, migrations, and required config keys. Five minutes of reading prevents an hour of archaeology.
We keep a sticky note in each service folder: last known-good tag, link to release notes, and whether the DB migrates automatically. If the note is blank, we do not pull.
Pre-update checklist (sticky note pattern)
Every compose project on our Proxmox hosts gets a `STACK.md` beside the compose file — the digital sticky note. Before any pull, we copy this block into the file and fill it in. Blank fields mean stop.
## Pre-update — YYYY-MM-DD
- Service:
- Current image (tag + digest):
- Target release / tag:
- Changelog link:
- DB migration? (auto / manual / none)
- Backup job name + last verified snapshot ID:
- Rollback digest (previous known-good):
- User journey to test:
- Scope frozen? (yes = one service only)The digest line matters more than the tag. Tags like `:release` can move on Docker Hub. After a successful night we paste the digest from `docker inspect` into STACK.md so rollback is copy-paste, not memory.
Snapshot first — and verify it exists
Snapshot volumes or run your backup job before updating. Confirm the snapshot exists. Then update.
Confirm means: check exit code, object count, or restic snapshot id — not "the timer fired." Silent backup failures are how upgrade nights become restore nights.
On our benches, restic runs against an S3-compatible bucket from a timer container. Before `docker compose pull`, we run:
restic -r s3:https://s3.example.com/homelab-backups snapshots --latest 1
restic -r s3:https://s3.example.com/homelab-backups check --read-data-subset=1/100For Proxmox VMs that hold compose stacks, we also snapshot the disk before touching databases:
qm snapshot 120 pre-upgrade-immich-$(date +%F)If restic check fails or the snapshot list is empty, the upgrade night ends there. Fixing backup beats fixing a half-migrated Postgres.
Pin by digest in compose
Floating `:latest` is for demos. Production-ish homelab services get a tag we read about, plus the digest we actually pulled:
services:
vaultwarden:
image: vaultwarden/server:1.32.5@sha256:abc123def456...
# digest from: docker inspect vaultwarden/server:1.32.5 --format '{{index .RepoDigests 0}}'After `docker compose pull`, run inspect again. If the digest changed unexpectedly, diff the release notes before you `up -d`. We record both tag and digest in STACK.md — tags for humans, digests for rollbacks.
One service at a time
Update the reverse proxy separately from databases when possible. Correlated failures are harder to debug.
Order that forgives mistakes: proxy config → app containers → database only when the release notes demand it. Never `docker compose pull` the entire rack in one breath on a Friday.
On a typical Proxmox node we treat each compose directory as its own blast radius. Pulling Immich does not mean pulling Vaultwarden in the same session unless the release notes explicitly couple them.
Verify user journeys
Log in, create a record, open a known file, check mobile if relevant. Healthchecks are necessary but not sufficient.
For Immich we open a recent photo and confirm a new upload from a phone; for Vaultwarden we unlock, sync once, and check an org item; for a git forge we push a tiny commit. If the journey fails, roll back before you "just fix forward" into a longer outage.
Failed upgrade story: Immich on a compose host
Lab note from an Orivana bench (2026-06): we bumped Immich to a release that shipped a breaking Postgres migration. The container started, `/api/server/ping` returned 200, and we almost called it done. Mobile upload failed with a generic error; logs showed `migration failed` on a column rename.
We had the previous digest in STACK.md from the last good night. Rollback was boring on purpose:
cd /opt/compose/immich
# restore compose to previous digest (edit image line or git checkout compose.yaml)
docker compose down
docker compose up -d
# verify journey: open known photo, upload test from phoneBecause we snapshot the VM disk *and* had a restic dump of the DB volume taken ten minutes earlier, we could have restored the volume if rollback alone was not enough. We did not need it — but the checklist is why we slept.
Vaultwarden has a similar failure mode: app starts, admin panel loads, but mobile clients reject sync after a schema tweak. Same rule: journey test, then declare victory.
Know the rollback
Document how to pin the previous image digest and restore volumes. Practice once so rollback is not theoretical.
Rollback script shape we keep in STACK.md:
# 1) edit compose image: to previous digest
# 2) down + up
docker compose down && docker compose up -d
# 3) if DB corrupted: restore volume from restic (see restore drill playbook)
restic -r s3:https://s3.example.com/homelab-backups restore latest --target /tmp/restore-test --include /immich/postgresPinning by digest (not only floating tags) made rollbacks boring. Tag-only pins sometimes moved under us when registries retagged.
A ninety-minute upgrade night template
This is the calendar block we actually use. One service. Phone on silent except for family.
**0–10 min — Read and freeze scope.** Open STACK.md, fill the pre-update checklist, read release notes, confirm no travel tomorrow.
**10–25 min — Backup and verify.** Run restic snapshots + check (or your equivalent). Proxmox snapshot if the service owns a database. Write the snapshot ID in STACK.md.
**25–50 min — Update one service.** `docker compose pull` for that directory only. Inspect digest. `docker compose up -d`. Watch logs for migration banners — not just "started".
**50–70 min — User-journey checks.** Login, create, sync, mobile if applicable. Hit the health URL from outside the container network if the service is public.
**70–90 min — Declare or roll back.** Update STACK.md with the new digest, or revert compose and `up -d` with the previous digest. Do not start a second service after a messy rollback. File a one-line note: what broke, what you learned.
If you finish under 70 minutes, use the spare time to update the family handoff doc — not to pull "just one more" container.
Related reading
See Docker Compose hardening, Backup strategy, Restore drill playbook, Immich operations review, and Vaultwarden home deployment.
Explore more
Related guides
- Self-Hosted RSS: Feed Readers, Sync, and OPML Hygiene
When to self-host FreshRSS or Miniflux, how to keep OPML portable, and fetch habits that respect publishers without turning RSS into another inbox.
- Self-Hosted Wiki for Households and Tiny Teams
Pick BookStack vs Wiki.js vs Outline for the job, structure pages people will actually use, and back up content before the wiki becomes the only copy.
- Ollama on a Homelab: Local LLMs Without Melting the Rack
Operator guide to running Ollama at home — install paths, Docker, model disk gravity, GPU vs CPU, API exposure, and a sane first weekend with Open WebUI.
- Docker Compose Hardening Checklist for Home Servers
Practical defaults for networks, secrets, updates, and least privilege on a personal Docker host — with lab notes from real breakages.