Practical guide

How to deploy database migrations safely

Change the schema so both the old and new application versions work during deployment. Leave destructive cleanup for a later release.

25 minutes · Doctrine Migrations

First, the short version

A migration is part of deployment, not just an SQL file

A database migration moves a schema and sometimes stored data from one known state to the next. In production, old and new application instances may run at the same time for a while.

A safe approach therefore separates schema expansion, data movement, and removal of the old structure. Each phase can be deployed independently, observed, and kept compatible with the adjacent application version.

Get ready

What you need

Before running a migration, understand its effect on locks, data, and the application serving traffic during the change.

  • Versioned migrations and the same Doctrine Migrations version in testing and production.
  • A production-like data volume on which to measure runtime and database lock duration.
  • A verified backup and a concrete restore procedure with an estimate of how long recovery takes.
  • Monitoring for application errors, database locks, latency, and free space during deployment.

Steps 1 to 3

Split the change into safe releases

Use the expand–migrate–contract sequence. Leave enough time between stages to verify production data.

1. Expand: add compatible structure

  1. Add a new column as nullable or with a safe default. The old code must continue to work after this change.
  2. Add a new table or index separately from removing old objects. For a large table, find out whether the operation blocks writes.
  3. Deploy an application that can read the old state and writes the new state, or temporarily writes to both structures.
  4. Always review the generated diff manually. Remove unexpected DROP or ALTER operations and add the conditions the migration actually needs.
php bin/console doctrine:migrations:diff
Official Doctrine documentation for generating migrations

2. Migrate: move data in batches

  1. Do not run a large data backfill as one long UPDATE in a deployment migration. Move data with a restartable command in small batches.
  2. Progress by a stable key and store the checkpoint. Repeated runs must be idempotent and must not damage rows already migrated.
  3. Watch latency, replication, and locks between batches. Reduce the batch size if the move affects normal traffic.
  4. Compare counts and validation queries when complete. Only then switch application reads to the new structure.
php bin/console app:backfill-new-column --batch-size=500
Official PostgreSQL documentation for changing tables

3. Contract: clean up only after verification

  1. Remove dual writes and old-format code only after every instance reads the new state and the data is complete.
  2. Add NOT NULL or foreign keys with regard to table size and the validation method supported by the specific database.
  3. Remove the old column or table in a separate later release. This preserves the option to redeploy the previous application quickly.
  4. Read the SQL with dry-run before execution and prepare stop, forward-fix, and restore plans. Do not rely blindly on a down migration.
php bin/console doctrine:migrations:migrate --dry-run
Official Doctrine documentation for managing migrations

Step 4

Verify the migration before and after production

Syntax checks are not enough. Test mixed application versions, real data volume, and operational impact.

  1. Run the migration on a copy of production data

    Measure time, locks, and space. At the same time, send normal reads and writes that will run during the production deployment.

    php bin/console doctrine:migrations:migrate --no-interaction
  2. Verify compatibility of both application versions

    After the expand phase, run smoke tests with the old and new builds. Both must work with the same intermediate schema state.

  3. Check the state after the production run

    Verify applied versions, data validation queries, errors, latency, and waiting locks before allowing deployment to continue.

    php bin/console doctrine:migrations:status

If something goes wrong

Common problems

ALTER TABLE blocks traffic for too long

Stop later deployment stages, inspect waiting locks, and use an online strategy appropriate for the database. Measure the operation on an equally large table first.

The new application expects a column that does not exist yet

The order is reversed. Deploy a compatible expand migration first, then the application, and leave the destructive contract phase for a later release.

The backfill stopped halfway through

Continue from the stored stable key. Make the backfill idempotent, batched, and able to leave an already migrated row unchanged.

A down migration would lose data during rollback

Do not run a destructive down automatically. Redeploy the compatible application, prepare a forward fix, or recover data with the previously verified restore plan.

Done

The migration is ready for a safe deployment.

The database migration has small compatible steps, measurable impact, and a concrete recovery plan. Use the same checklist before every production schema change.

Request a call

I will call you on the next working day between 9:00 and 17:00.

You can also call me directly.

+420 605 181 728

Leave your phone number and send a callback request.

By sending, you agree to processing your data in order to handle your request.