Skip to main content

Migrate a legacy Docker Compose installation

Use this guide when the existing installation has a docker-compose.yml with services such as app, db, and minio, or when it has historically been updated with docker compose pull. This is a one-time layout migration. It is not the same as a routine upgrade of an installation that already uses docker-compose.prod.yml and ./casebender upgrade.
Do not continue without a tested PostgreSQL restore and a verified attachment backup. Never run ./casebender init, replace the existing .env, rotate an existing encryption or audit key, or run docker compose down -v.

What changes

Docker Compose prefixes named volumes with the Compose project name. Keeping the same installation directory and project name normally lets the signed bundle reuse pgdata and redis_data. A different directory or -p value creates different volume names and can make the application appear empty even though the original data still exists.

Phase 1: Inventory the existing installation

Run these commands from the existing installation directory:
Record:
  • the Compose project name and installation directory;
  • the exact CaseBender image tags;
  • the PostgreSQL image and major version;
  • the actual volume names mounted at /var/lib/postgresql/data, /data, and /app/apps/web/app/secret;
  • whether attachments use MinIO, local storage, or an external provider;
  • the current database user, database name, and internal hostname;
  • the current TLS and reverse-proxy configuration.
Inspect mounts without printing environment secrets:
Check required keys by name only:
Do not paste .env, license keys, encryption keys, database URLs, or backup contents into tickets or chat.

Phase 2: Create and test the recovery set

PostgreSQL

Create a logical backup using the legacy db service:
The final proof is a restore into an isolated staging database followed by application validation. Listing the archive is not a restore rehearsal. Record baseline counts for users, organizations, cases, alerts, tasks, attachments, evidence, and audit records. Use approved read-only queries for the deployed schema.

Configuration, TLS, and secrets

Store the backup outside the Docker host in the approved encrypted recovery system. Include:
  • .env;
  • TLS certificates, private keys, and custom trust stores;
  • the exact license secret and license blob;
  • field and credential encryption keys;
  • webhook, connector-signing, OAuth, authentication, and audit secrets;
  • integration configuration and proxy/egress settings;
  • the current Compose file and image inventory.
If LICENSE_SECRET_KEY is absent from .env, preserve the existing value before stopping app:
Transfer that value into the protected LICENSE_SECRET_KEY entry during the environment transformation. Do not print it.

Attachments

If STORAGE_PROVIDER=minio, back up the bucket through the S3/MinIO API. A tarball or direct copy of MinIO’s internal miniodata layout is a disaster recovery snapshot, not a valid local-storage migration. Keep both:
  1. a snapshot or archive of the original miniodata volume; and
  2. an object-level export made with mc mirror or your approved S3 backup process.
Compare the exported object count and size with the source, then download several known case attachments and evidence files from the rehearsal system.

Phase 3: Check alert-promotion compatibility

The new release enables alert-to-case promotion by default. Keep it explicitly disabled during a legacy-data migration:
Check the schema and legacy observable parentage:
Stop and use the release-specific promotion backfill procedure when:
  • the promotion table is missing;
  • any dual-parent observable exists; or
  • backfill verification is not clean.
Do not enable promotion until the schema migration is applied, dual-parent conflicts are zero, the parent constraint decision is verified, and the worker outbox processor is healthy.

Phase 4: Choose the attachment target

Option A: Continue using an external MinIO or S3-compatible service

This minimizes application-level storage change. The object store must remain reachable from the new web container. Set:
MINIO_ENDPOINT is a hostname, not a URL. Use a trusted TLS endpoint for production. The signed bundle does not run an embedded MinIO container, so an external endpoint must exist before cutover.

Option B: Migrate MinIO objects to local /data

Create the target volume with the same Compose project name:
Use mc mirror or an equivalent object-level export to write each bucket object under the same object key in casebender_data. Do not copy MinIO’s internal volume files directly. Set:
Validate object counts, total bytes, and representative downloads in staging. Retain the original miniodata backup through the rollback window.

Phase 5: Prepare .env for the signed bundle

Start from the existing .env; do not start from .env.example and do not run ./casebender init. The release’s ./casebender upgrade copies CASEBENDER_RELEASE_VERSION and CASEBENDER_REGISTRY from the verified release.env, and backfills missing audit and integration secrets. Never replace an existing value during a routine migration. Update NO_PROXY for the new internal names, including postgres, redis, web, api, worker, and other enabled services.

Phase 6: Rehearse the complete migration

Restore copies of PostgreSQL, attachments, .env, and secrets into an isolated network. Use the same PostgreSQL major version as the source. In rehearsal:
  1. verify and extract the signed bundle;
  2. preserve the intended Compose project name;
  3. apply the .env, TLS, database-hostname, Redis, storage, and license changes;
  4. run preflight;
  5. start the pinned release;
  6. verify migrations and service health;
  7. compare baseline database and attachment counts;
  8. validate login, cases, alerts, tasks, attachments, evidence, credentials, integrations, audit writes, and backups;
  9. complete the alert-promotion backfill checks before enabling promotion;
  10. record the actual recovery point and recovery time.
Do not connect the rehearsal environment to production integrations.

Phase 7: Production cutover

  1. Announce a maintenance window and stop inbound integrations and user writes.
  2. Take fresh final PostgreSQL and attachment backups.
  3. Preserve the legacy Compose file as docker-compose.legacy.yml.
  4. Verify and copy the new bundle files into the same installation directory.
  5. Preserve .env and apply the reviewed transformation.
  6. Install trusted TLS files at:
  7. Stop the legacy stack without deleting volumes:
  8. Run:
  9. Keep ENTERPRISE_ALERT_PROMOTION=disabled until post-migration schema and backfill verification passes. Then remove the override or set it to enabled, recreate the caller services, and test a non-critical promotion.

Phase 8: Validate and close the migration

Confirm:
  • every expected container is healthy;
  • the deployment remains ACTIVE and existing users can sign in;
  • baseline users, organizations, cases, alerts, tasks, audit records, and attachment counts match;
  • representative attachments and evidence download correctly;
  • stored integration credentials still decrypt and a safe connection test succeeds;
  • Redis, worker queues, and the alert-promotion outbox are healthy;
  • updating a non-critical alert creates an audit entry without an integrity error;
  • creating and merging a non-critical alert into a case succeeds after promotion is enabled;
  • the deployment can produce a new backup.
Run the release canary:
Retain the legacy Compose file, previous image manifest, original .env, database dump, object-store backup, and volume snapshots until the approved rollback window closes.

Rollback

Before starting the signed bundle, restart the legacy Compose file against the untouched volumes if rehearsal or preparation fails. After database migrations run:
  • use ./casebender rollback --confirm-schema-compatible only when the release notes explicitly permit image-only rollback;
  • otherwise stop the new stack, restore the matching pre-migration PostgreSQL and attachment backups, restore the legacy .env and Compose file, and start the previous pinned images.
Never run an older application image against an unsupported newer schema. After this one-time migration succeeds, follow Upgrading CaseBender for future releases.