Tech Digest

Guide

An update strategy that does not lose data

Unattended updates of stateful containers are the single most common way self-hosters destroy their own data. Notification plus a scheduled window costs twenty minutes a month and removes the entire failure class.

Last reviewed

Should you automatically update self-hosted Docker containers?

No, not for anything that holds state. Container upgrades routinely run forward-only database migrations on startup, and many projects explicitly do not support downgrading, so an unattended pull at 4am can leave you with a migrated database that the previous image cannot read. Run a notify-only tool such as Diun, pin every image to an explicit tag, and upgrade in a scheduled window after taking a filesystem snapshot or a database dump. Automatic updates are reasonable for stateless containers and for OS security patches, which are a separate question.

Here is the position, stated before the reasoning: do not let anything update your stateful containers automatically. Run a tool that tells you an update exists, pin your tags, and upgrade deliberately in a window you chose, after taking a snapshot. The cost is about twenty minutes a month. What it buys you is the removal of an entire category of unrecoverable failure.

Why unattended container updates destroy data#

The mechanism is not exotic. Most self-hosted applications run database migrations on startup, forward only. When an automatic updater pulls a new image and recreates the container at 4am, the migration runs. You did not read the release notes, you did not take a snapshot, and the previous image can no longer open the schema it now finds.

The projects say this themselves:

  • Immich's upgrade documentation states that downgrading to an earlier version is unsupported even within the same minor, because migrations run forward on startup. The documented recovery path from a bad upgrade is restoring the database dump you took beforehand.
  • Uptime Kuma's v1 to v2 upgrade rewrites every heartbeat row into aggregate tables. There is no downgrade path, and the wiki says an interrupted migration means restoring from backup and starting again.
  • Grafana runs schema migrations automatically on start and offers no downgrade. Reverting the image against a migrated database usually fails to start.
  • Jellyfin 10.11's database migration is one way.

Add the second mechanism: floating tags. If your compose file says latest, an automatic updater is not applying a patch release, it is applying whatever the maintainer last built, including major versions. That is how a Nextcloud instance ends up two majors ahead of where it can migrate from, and how a wg-easy v14 deployment meets v15, a full rewrite whose maintainer answered the question about a migration path with "there is and will be none".

Pin the tag, and know what pinning means#

Two rules, because databases and applications differ.

Applications: exact versions. ghcr.io/paperless-ngx/paperless-ngx:3.0.0, not :3 and never :latest. The tag is a record of what is running and a rollback target. Bump it in git, so git log on your stacks directory is an upgrade history.

Databases: pin the major. postgres:17 picks up patch releases, which are safe, and never crosses into 18, which would require a dump and reload rather than a restart. A Postgres container that finds a data directory from an older major refuses to start, which is the correct behavior and a bad surprise at 2am.

There is one common exception to "pin and forget": client-server pairs where the client updates itself. Vaultwarden is the example. Bitwarden's browser extensions and mobile apps auto-update and periodically require a newer server; release 1.37.2 carries an explicit warning that it is required for clients on 2026.8.0 and later. Pinning the server and letting clients float produces login failures that look like your problem. Subscribe to that project's releases specifically.

The projects whose release notes you actually have to read#

Most upgrades are uneventful. A small set are not, and knowing which is most of the skill:

ProjectWhat bites
NextcloudCannot skip a major. 31 to 34 is three separate upgrades. PHP version is coupled to the Nextcloud version, and incompatible third-party apps block the upgrade.
ImmichForward-only migrations, downgrade unsupported. v3.0.0 removed pgvecto.rs support, so older instances must reach a VectorChord-capable release first.
HeadscaleYou cannot skip minors. 0.28 removed migrations older than 0.25.0. The minimum Tailscale client version keeps moving.
Proxmox VE8 to 9 requires every node on the latest 8.4 and pve8to9 --full clean before you touch the sources list.
BorgBackupBorg 2 repositories are incompatible with 1.x. You do not upgrade a repo, you borg transfer --other-repo into a new one, needing space for a second copy.
AutheliaThe 4.38 configuration change was a rewrite, not a tag bump: session and redirection keys moved.
Paperless-ngxAn export can only be imported into the same version it came from, so store the image tag next to every export.
Home AssistantShips monthly and deprecates aggressively; one custom component that fails to load can prevent startup.

The pattern to notice: the projects that break are the ones with a real database and a fast release cadence. Anything at operational load 1 on this site, a single container with a file-shaped data surface, is genuinely safe to upgrade casually. Across the 105 tools profiled here, 60.0% score 2 or lower, and those are the ones where "pull and restart" is a reasonable habit.

Snapshot before you upgrade, every time#

This is the step that turns a bad upgrade into a twenty minute inconvenience. On ZFS or btrfs it is nearly free, because a snapshot is metadata until something changes.

bash
#!/usr/bin/env bash
# /usr/local/sbin/upgrade-stack.sh <stack>
set -euo pipefail
STACK="$1"
DIR="/srv/stacks/$STACK"
STAMP="$(date +%Y%m%d-%H%M)"
cd "$DIR"

# 1. Dump the database while the OLD version is still running.
docker compose exec -T db pg_dumpall -U postgres \
  | gzip > "/srv/backups/${STACK}-${STAMP}.sql.gz"

# 2. Record exactly what is running now, so rollback has a target.
docker compose config --images > "/srv/backups/${STACK}-${STAMP}.images"

# 3. Filesystem snapshot of the whole data tree.
btrfs subvolume snapshot -r /srv/data "/srv/.snapshots/data-${STAMP}"
# ZFS equivalent:  zfs snapshot tank/srv@"${STAMP}"

# 4. Now upgrade, having edited the image tag in compose.yaml first.
docker compose pull
docker compose up -d
docker compose ps

If your filesystem does not do snapshots, the dump is not optional, it is the whole safety net. ZFS, btrfs, mdadm or one disk covers the filesystem choice, and Backing up a running database covers dumping a running database without corrupting it, which matters because 81.9% of the tools profiled here cannot be safely copied as live files.

Rollback that works, versus rollback that leaves a migrated database behind#

There are two rollbacks and people confuse them constantly.

Rollback that works: the new version did not migrate anything. Change the tag back in compose.yaml, docker compose up -d, done in thirty seconds. This covers most reverse proxies, most stateless front ends, and anything storing plain files.

Rollback that does not: the new version migrated your data on startup. Putting the old image back now gives you one of three outcomes, in descending order of luck: it refuses to start with a schema version error, it starts and silently misbehaves, or it starts and migrates something else in the wrong direction. The only correct recovery is: stop the stack, roll the snapshot back or restore the dump, then start the old tag.

That distinction is why the snapshot happens before the pull, and why "I can always roll back" is only true if you did the work in advance. Test the assumption once, on something unimportant, so you know what your own rollback actually looks like.

The operating system is a different question#

Automatic OS security updates are good practice and not in tension with anything above. On Debian, the default unattended-upgrades configuration installs security updates and not feature updates. Distribution security patches are deliberately backported to avoid changing behavior, which is the opposite of an upstream container release.

bash
sudo apt install unattended-upgrades apt-listchanges
sudo dpkg-reconfigure -plow unattended-upgrades
# Then review the origins in /etc/apt/apt.conf.d/50unattended-upgrades,
# and put local overrides in 52unattended-upgrades-local so upgrades do not clobber them.

Two things to set while you are there: send the mail or notification somewhere you read, and decide deliberately about automatic reboots. A server that reboots itself at 3am to apply a kernel update is fine if your stacks come back cleanly, and is a silent outage if one of them does not. Test a reboot manually before you let a cron job do it, and see The minimum viable monitoring stack for making sure you find out.

Note also that Docker Engine itself is an OS-level package and mostly invisible to upgrade, with the exception of majors: Docker Engine 29.0 raised the minimum client API version to v1.44 and broke management tools compiled against older versions, and 29.3.0 lowered it back to v1.40.

Watchtower and Diun: the verdict#

Diun is the right default. It watches registries and notifies. It cannot update anything, which means there is no configuration mistake that turns it into an unattended upgrader. Its Docker provider defaults to watchByDefault: false, so containers are opted in with a diun.enable=true label rather than opted out.

yaml
# /srv/stacks/diun/compose.yaml
name: diun
services:
  diun:
    image: crazymax/diun:4
    restart: unless-stopped
    command: serve
    environment:
      TZ: Europe/Berlin
      DIUN_WATCH_SCHEDULE: "0 8 * * 1"        # Monday 08:00, once a week is enough
      DIUN_WATCH_FIRSTCHECKNOTIF: "false"
      DIUN_PROVIDERS_DOCKER: "true"
      DIUN_NOTIF_NTFY_ENDPOINT: "https://ntfy.example.com"
      DIUN_NOTIF_NTFY_TOPIC: "diun"
    volumes:
      - /srv/data/diun:/data
      - /var/run/docker.sock:/var/run/docker.sock:ro

Then add labels: ["diun.enable=true"] to the services you want watched. Note the socket mount: read-only on the mount does not make the Docker API read-only, and anything that can talk to that socket can create a privileged container. Put a socket proxy in front of it, as A security baseline for a home server describes.

Watchtower is defensible only in monitor-only mode. Set WATCHTOWER_MONITOR_ONLY=true and it notifies without updating, which makes it a slightly heavier Diun. Its default behavior, pulling and recreating containers on a schedule, is the thing this page is arguing against. If you keep it in updating mode, scope it explicitly with com.centurylinklabs.watchtower.enable labels on containers that hold no state, and accept that you have chosen convenience over recoverability for those. Watchtower vs Diun goes through the details.

The one case for real auto-updates: a stateless container behind a proxy, where the recovery action is "pull the previous tag" and nothing on disk changes. Reverse proxies and static site containers qualify. A photo library does not.

The twenty minute monthly window#

  1. Read the Diun notifications you have been collecting. Discard anything you do not care about.
  2. For each service you will upgrade, open the release notes and search for "breaking".
  3. Edit the image tag in compose.yaml, commit the change.
  4. Run the snapshot script.
  5. docker compose pull && docker compose up -d, then actually open the application and click something.
  6. Leave the snapshot for a week before deleting it, because migration damage often surfaces on the second or third day.

What to do next#

Replace every latest in your stacks with the tag you are currently running, which takes an afternoon and immediately removes the worst failure mode. Then deploy Diun, and write the snapshot script before your next upgrade rather than after it. If your compose layout is not yet consistent, Docker Compose conventions is the prerequisite.

Questions#

Is Watchtower safe to use?

Watchtower is well-behaved software and its default mode is the problem, not its code. Pulling and recreating stateful containers on a schedule means schema migrations run while you are asleep, on a version you did not choose, with no snapshot taken first. If you want to keep it, run it with WATCHTOWER_MONITOR_ONLY=true so it notifies without updating, or scope it with the com.centurylinklabs.watchtower.enable label to genuinely stateless containers only.

What is the difference between Watchtower and Diun?

Watchtower can update containers; Diun cannot. Diun watches registries and tells you a new tag or digest exists, and that is its entire feature set. That limitation is the reason to prefer it: there is no configuration mistake that turns Diun into an unattended upgrader. Diun also watches by label (diun.enable=true) by default, so you opt containers in rather than out.

Should I pin to a major version tag or an exact version?

Exact versions for applications, major tags for databases. An application's :3.0.0 tells you precisely what is running and what to roll back to. A database image on :17 picks up safe patch releases and never crosses a major boundary, which matters because a Postgres major upgrade needs a dump and reload rather than a container restart. Never use latest for either.

How do I roll back a container upgrade?

Change the tag back and restart, which works only if the new version did not migrate your data. If it did, the rollback is: stop the stack, restore the pre-upgrade snapshot or database dump, then start the old tag. That is why the snapshot comes before the pull. Without it, reverting the image against a migrated database usually fails to start, or, worse, starts and behaves strangely.

Should I enable unattended-upgrades on the host OS?

Yes, for security updates. On Debian the default unattended-upgrades configuration installs security updates and not feature updates, which is exactly the risk profile you want on a machine you touch monthly. It is a different question from containers because distribution security patches are backported specifically to avoid changing behavior, whereas an upstream container release is a new version of the application.

Which projects genuinely cannot skip a major version?

Nextcloud is the strictest: the admin manual requires you to reach the latest point release of your current major, then move to the next major, one at a time. Headscale requires stepping through each stable minor in order and removed migrations for anything older than 0.25.0 in 0.28. Immich must reach a VectorChord-capable release before v3.0.0. Proxmox VE requires every node on the latest 8.4 and a clean pve8to9 --full before 8 to 9. Check before you jump.

How often should I actually update?

Monthly for most things, immediately for anything with a published security advisory affecting a service you expose, and never on the day a major lands. Waiting two weeks after a .0 release costs you nothing and lets other people find the migration bugs. The exception is a client-server pair such as Vaultwarden, where Bitwarden's own clients auto-update and can outrun a pinned server.

Sources#

Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.