Tutorial

Updating a Docker Compose stack in production

Deployment9 min read5 steps

A `docker compose pull` followed by `up -d` has always worked — until it doesn't. The recent breaking changes in Meilisearch v1.54, Supabase PostgreSQL 15→17, Langfuse v3→v4, and NocoDB 2026.09 confirmed it the hard way: stable, months-old production instances brought down by an unprepared update. This guide gives you a repeatable protocol — back up before you pull, check release notes before restarting, and roll back to the previous image in under two minutes if something breaks.

Contents· Why `latest` is the real culprit1/9
  1. 01Why `latest` is the real culprit
  2. 02What this guide covers — and what it does not
  3. 03Step 1 — Pin all your images
  4. 04Update protocol — the 5 steps
  5. 05Keep the previous version available locally
  6. 06Recent breaking changes that took down instances
  7. 07Update strategies: comparison
  8. 08Integrating this protocol into your workflow
  9. 09Automate without losing control

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), configuring depends_on and service_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.0

Once 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

  1. 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 manual pg_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.

  2. 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 healthy before dumping:

    docker compose exec db pg_dump -U postgres -Fc mydb > backup_$(date +%Y%m%d_%H%M%S).dump

    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_data

    Verify the backup is readable before proceeding. A corrupt dump file discovered during recovery is the most expensive scenario there is.

  3. 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 meilisearch

    If 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 meilisearch

    Wait for the healthcheck to reach healthy before validating. A service that starts but is not yet healthy is not a ready service.

  4. 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: 30s

    The start_period field is critical for slow-starting services (databases, search engines): it prevents Docker from declaring the container unhealthy during the initialization phase and triggering a premature restart.

    See docker-compose-depends-on-healthcheck for the full service_healthy configuration on PostgreSQL — the same principle applies to any service that needs a warm-up period.

  5. 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 (or git revert if you committed the bump).
    2. Restart only the affected service, without recreating volumes:

    docker compose up -d --no-deps --force-recreate meilisearch

    3. Check the logs immediately:

    docker compose logs -f meilisearch

    The --no-deps flag is essential: it restarts the target service without touching other containers (database, cache, proxy). Without it, docker compose up -d may 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:rollback

This 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

StrategyData safetyPrep timeRollback
`docker compose pull` + `up -d` directNo guarantee — breaking changes undetected< 1 minuteDifficult if data was migrated
Versioned tag bump + volume backupHigh — data saved before any change10 to 20 minutesTrivial: revert tag + `up -d --no-deps`
Staging test before prodMaximum — breaking changes caught outside prodVaries by environmentNot needed if test passed
Image pinned by SHA256 digestHigh — immune to tag-overwriteSame as versioned tagSame 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.

A VPS with root access to apply this protocol

Full dumps, snapshots before updates, rollback to a previous image: this protocol requires root access and controllable local storage. Shared hosting does not give you this level of control over Docker volumes.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab