Why `latest` is the real culprit
When a Docker image tagged latest is updated in the registry, your docker compose pull downloads it silently. No warning, no diff. You restart the stack and discover the new container cannot read the data left by the old one.
This is exactly what happened with Meilisearch v1.54. This version introduced a new default vector storage format (switch from arroy to HNSW) that makes the data directory incompatible with older versions. On startup, Meilisearch refuses to open the database and enters a crash-loop. The only clean recovery path is to create a dump before the update — impossible once the container is stuck.
The latest tag does not resolve to the same image depending on when you pull. Two developers running docker compose pull twelve hours apart may pull different versions. On a production stack, this ambiguity is unacceptable. The solution is not to avoid updates: it is to explicitly control which version is running and to consciously decide when to move to the next one.
What this guide covers — and what it does not
- What this guide covers: a step-by-step protocol to update a Docker Compose stack on a VPS — version pinning, volume backup, release notes review, fast rollback, healthchecks as a safety net.
- Covered elsewhere: the initial hardening checklist (
docker-compose-production-checklist), configuringdepends_onandservice_healthy(docker-compose-depends-on-healthcheck), and comparing auto-update tools like Watchtower or Diun. - What this guide does not recommend: Watchtower or any auto-pull tool in production — that is precisely the anti-pattern the breaking change cases illustrate.
- Target audience: developers and agencies managing one or more Docker Compose stacks in production on a VPS, with root access and persistent volumes.
Step 1 — Pin all your images
The first action, before any update, is to replace every image: meili/meilisearch:latest or image: supabase/postgres with an explicit version.
Two forms are acceptable:
- Version tag: image: getmeili/meilisearch:v1.53.0 — readable, versionable in git, easy to patch.
- SHA256 digest: image: getmeili/meilisearch@sha256:abc123… — immutable, guarantees you pull exactly the same artifact on every redeploy, even if the tag has been overwritten (which happens on public registries).
To get the digest of an already-running image:
docker inspect --format='{{index .RepoDigests 0}}' getmeili/meilisearch:v1.53.0Once your images are pinned, commit docker-compose.yml to git. Every version bump becomes a commit, giving you a clear history and a trivial rollback (git revert + docker compose up -d).
Update protocol — the 5 steps
Read the release notes before pulling
First, check the release notes for the new version. Look for the words
breaking,migration,incompatible,pg_upgrade,dump. This is not optional: Supabase explicitly documented that upgrading from PostgreSQL 15 to 17 requires a manualpg_upgrade— the PG 17 container refuses to start on a PG 15 volume, and the initialization process does not automatically migrate data.For Langfuse v4 (released August 17, 2026), Python SDK v2 and older are rejected at ingestion by the new stack — a breaking change that affects all client services tracing via the old API.
Three minutes of reading saves several hours of data recovery.
Back up volumes before pulling
Never pull before having a usable backup. For named volumes, two approaches:
Application dump (recommended for databases) — the service must be
healthybefore dumping:docker compose exec db pg_dump -U postgres -Fc mydb > backup_$(date +%Y%m%d_%H%M%S).dumpRaw volume snapshot — useful for binary stores (Meilisearch, Redis, MinIO):
docker run --rm \ --volumes-from $(docker compose ps -q meilisearch) \ -v $(pwd)/backups:/backup \ alpine tar czf /backup/meili_$(date +%Y%m%d_%H%M%S).tar.gz /meili_dataVerify the backup is readable before proceeding. A corrupt dump file discovered during recovery is the most expensive scenario there is.
Pull the new image and test offline
Update the tag in your
docker-compose.yml, then pull the image without restarting the service:docker compose pull meilisearchIf your environment allows it, test the new image on a volume clone in a ddev environment or a staging VM before touching production. Check the startup logs for any migration errors:
docker compose up -d meilisearch docker compose logs -f meilisearchWait for the healthcheck to reach
healthybefore validating. A service that starts but is not yethealthyis not a ready service.Check healthchecks
A well-configured healthcheck is your first detection line. It must be present on every critical service in the stack, in Compose v2 format:
healthcheck: test: ["CMD-SHELL", "curl -sf http://localhost:7700/health || exit 1"] interval: 10s timeout: 5s retries: 5 start_period: 30sThe
start_periodfield is critical for slow-starting services (databases, search engines): it prevents Docker from declaring the containerunhealthyduring the initialization phase and triggering a premature restart.See
docker-compose-depends-on-healthcheckfor the fullservice_healthyconfiguration on PostgreSQL — the same principle applies to any service that needs a warm-up period.Roll back if something breaks
If the new version fails to start or produces errors, rollback must take under two minutes. The procedure:
1. Revert to the previous tag in
docker-compose.yml(orgit revertif you committed the bump).
2. Restart only the affected service, without recreating volumes:docker compose up -d --no-deps --force-recreate meilisearch3. Check the logs immediately:
docker compose logs -f meilisearchThe
--no-depsflag is essential: it restarts the target service without touching other containers (database, cache, proxy). Without it,docker compose up -dmay recreate the entire stack.⚠️ If the new version migrated the on-disk data format (Meilisearch v1.54, Supabase PG17), rolling back the image is not enough — which is why the volume backup is a precondition, not an option.
Keep the previous version available locally
Before pulling the new image, tag the currently-running image under a retention name:
docker tag getmeili/meilisearch:v1.53.0 getmeili/meilisearch:rollbackThis lets you return to the exact production state in an emergency, even if you no longer have registry access or the connection is slow. On a VPS with limited bandwidth, this local tag saves several minutes of download time at the worst moment.
Recent breaking changes that took down instances
These four examples illustrate why the protocol above is not theoretical.
Meilisearch v1.53 → v1.54 (2026): introduction of the HNSW vector store as the default format. Meilisearch refuses to open an index created with the old arroy format. Startup enters a crash-loop with Your database version is incompatible with your current engine version. The only clean way out is to have exported a dump before the update — importing it into the new version restores your data.
Supabase Docker PostgreSQL 15 → 17 (migration activated June 17, 2026): the supabase/postgres:17 container cannot read a volume initialized by PG 15. Supabase explicitly documents that the jump requires a pg_upgrade via a dedicated script — the container initialization process does not do it automatically. Without a prior migration, the database does not start.
Langfuse v3 → v4 (GA August 17, 2026): v4 drops the legacy batch ingestion endpoints in favor of OpenTelemetry. Python SDK v2 and older and JS/TS SDK v3 and older are rejected at ingestion from the moment the v4 stack starts. If your client services have not migrated before the server update, they silently lose all their traces.
NocoDB 2026.09.x: the 2026.09 series rebuilds Docker images to remove vulnerable dependencies. Installations using bind-mounts (./postgres, ./nocodb) instead of named volumes may start on an empty database after the pull — NocoDB cannot find its data if the mount path changed between versions. Migrate to named volumes before updating.
Update strategies: comparison
Scroll the table
| Strategy | Data safety | Prep time | Rollback |
|---|---|---|---|
| `docker compose pull` + `up -d` direct | No guarantee — breaking changes undetected | < 1 minute | Difficult if data was migrated |
| Versioned tag bump + volume backup | High — data saved before any change | 10 to 20 minutes | Trivial: revert tag + `up -d --no-deps` |
| Staging test before prod | Maximum — breaking changes caught outside prod | Varies by environment | Not needed if test passed |
| Image pinned by SHA256 digest | High — immune to tag-overwrite | Same as versioned tag | Same as versioned tag |
Integrating this protocol into your workflow
A protocol that stays in a guide is useless. To apply it systematically, externalize the version into a versioned .env file committed to git:
# .env
MEILISEARCH_VERSION=v1.53.0
POSTGRES_VERSION=15.6# docker-compose.yml
services:
meilisearch:
image: getmeili/meilisearch:${MEILISEARCH_VERSION}Updating a version then becomes a single commit on .env — readable in git log, reversible with git revert, and deployable by CI/CD without modifying the main Compose file.
For agencies managing multiple client stacks, create an INFRA_CHANGELOG.md per client: every update is traced with the previous version, date, backup taken, and outcome. This also protects you contractually in the event of a subsequent incident.
Automate without losing control
If you want to be notified of new versions without auto-pulling, Diun (Docker Image Update Notifier) monitors your registry and sends you a notification (Slack, email, webhook) when a new image is available. You remain in control of when to update.
This is the fundamental difference from Watchtower: Diun notifies, Watchtower acts. On a production stack with persistent volumes, notification is the right level of automation — the action stays manual and preceded by the protocol above.