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 the risk with Meilisearch. The database carries the version number of the engine that wrote it: on a version upgrade, the new engine refuses to open it until you explicitly ask it to migrate with --upgrade-db (or the MEILI_UPGRADE_DB variable). With a restart policy, the container then enters a crash-loop. And once the database has been migrated, the old image can no longer open it: Meilisearch does not support downgrades. The only clean safety net is a dump or a volume snapshot taken before the update — impossible once the database is migrated or 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: getmeili/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 (v4.0.0 released July 29, 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 -T db pg_dump -U postgres -Fc mydb > backup_$(date +%Y%m%d_%H%M%S).dumpThe
-Toption disables the pseudo-terminal: without it, the binary dump stream can be altered.Raw 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 after
--upgrade-db, 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 (on a version upgrade): a newer engine refuses to open a database written by an older version, with Your database version (…) is incompatible with your current engine version, until --upgrade-db is passed. That migration can change the on-disk format: in v1.37.0 (March 2026), vector indexes still in the arroy format were converted to the new HNSW store (hannoy). And downgrades are not supported (Downgrade is not supported). The only clean way out after a failure is to have exported a dump before the update, then import it (meilisearch --db-path /meili_data --import-dump /backup.dump) — possible only if you exported before updating.
Supabase Docker PostgreSQL 15 → 17 (PG 17 default from 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 (v4.0.0 released July 29, 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 as soon as the v4 stack runs in its default write mode (events_only); the legacy and dual modes let you stage the migration. If your client services have not migrated before the server update, they silently lose all their traces.
NocoDB (2026.09 series): no broken database, but an incompatible change announced in advance. The 2026.09.0 notes repeat that datetimes returned by the REST API, webhooks and exports will move from second to millisecond precision on PostgreSQL and SQLite in an upcoming release. A client that parses dates with a strict format will break without the server reporting anything — exactly what reading the release notes lets you anticipate. Another trap, independent of the version: a bind-mount installation (./postgres, ./nocodb) whose mount path changes between two deployments starts on an empty database. Named volumes prevent this drift.
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.