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.
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
- Add a new column as nullable or with a safe default. The old code must continue to work after this change.
- Add a new table or index separately from removing old objects. For a large table, find out whether the operation blocks writes.
- Deploy an application that can read the old state and writes the new state, or temporarily writes to both structures.
- 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
- Do not run a large data backfill as one long UPDATE in a deployment migration. Move data with a restartable command in small batches.
- Progress by a stable key and store the checkpoint. Repeated runs must be idempotent and must not damage rows already migrated.
- Watch latency, replication, and locks between batches. Reduce the batch size if the move affects normal traffic.
- 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
- Remove dual writes and old-format code only after every instance reads the new state and the data is complete.
- Add NOT NULL or foreign keys with regard to table size and the validation method supported by the specific database.
- Remove the old column or table in a separate later release. This preserves the option to redeploy the previous application quickly.
- 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.
-
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 -
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.
-
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.