Why Not Migrate at App Startup
Calling context.Database.Migrate() in Program.cs is convenient for local development but risky in production: with multiple replicas starting simultaneously, such as during a Kubernetes rolling deployment scaling up new pods, each instance can attempt to apply migrations concurrently, causing lock contention on the __EFMigrationsHistory table or partial failures. A failed migration in this pattern can crash every pod's startup at once, turning a schema issue into a full outage.
Cricket analogy: It's like every fielder independently deciding to reposition the sightscreen the moment they walk onto the field — with several people acting at once, you get contention and confusion instead of one coordinated adjustment.
A Dedicated Migration Step in the Pipeline
A safer pattern generates dotnet ef migrations script --idempotent -o migrate.sql as a build artifact, then applies it through a dedicated pipeline job — a GitHub Actions step, an Azure DevOps release task, or a Kubernetes Job — that runs once, before the new application version is rolled out. This separates 'change the schema' from 'start new app instances' into distinct, independently retryable steps, so a migration failure blocks the deploy cleanly instead of crash-looping every pod.
Cricket analogy: It's like scheduling the ground staff's pitch relay work as a distinct task completed before the players even arrive, so a pitch problem is caught and fixed before it ever affects the actual match.
jobs:
migrate-database:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- run: dotnet tool install --global dotnet-ef
- run: dotnet ef migrations script --idempotent -o migrate.sql --project src/MyApp.Data
- name: Apply migration script
run: sqlcmd -S ${{ secrets.DB_SERVER }} -d MyAppDb -U ${{ secrets.DB_USER }} -P ${{ secrets.DB_PASSWORD }} -i migrate.sql
deploy-app:
needs: migrate-database
runs-on: ubuntu-latest
steps:
- run: echo "Deploy new app version only after migration job succeeds"Backward-Compatible Migrations for Zero-Downtime Deploys
During a rolling deploy, old and new versions of the application run simultaneously against the same database, so migrations applied ahead of the rollout must be backward-compatible with the version still running. Adding a new nullable column is safe, but renaming or dropping a column the old version still reads or writes will break it immediately. The standard technique is the expand-contract pattern: first expand by adding the new column and deploying code that writes to both old and new columns, then later contract by dropping the old column in a subsequent release once no running version depends on it anymore.
Cricket analogy: It's like widening a stadium's exit gates before removing the old ones — for a transitional period both old and new gates operate together, so the crowd already mid-exit is never left without a way out.
Practical checklist for zero-downtime migrations: prefer additive changes (new nullable columns, new tables); avoid renaming columns in a single step (add new, migrate data, drop old in a later release); avoid NOT NULL constraints on new columns until existing rows are backfilled; and always test the generated migration script against a production-like copy of the database before applying it to production.
- Calling Migrate() at app startup risks race conditions and crash-looping when multiple replicas start concurrently.
- A dedicated pipeline step applying an idempotent SQL script separates schema changes from app rollout.
- This separation makes migration failures block the deploy cleanly instead of crashing every app instance.
- Rolling deploys run old and new app versions simultaneously against the same database.
- Migrations must be backward-compatible with the still-running old version during a rolling deploy.
- The expand-contract pattern adds new schema elements first, then removes old ones in a later release.
- Testing migration scripts against a production-like database copy before applying them to production is essential.
Practice what you learned
1. What is the main risk of calling context.Database.Migrate() at application startup with multiple replicas?
2. What is the benefit of running migrations as a dedicated pipeline step rather than at app startup?
3. Why must migrations during a rolling deploy be backward-compatible?
4. What does the expand-contract pattern involve when renaming a column with zero downtime?
Was this page helpful?
You May Also Like
Applying and Reverting Migrations
How to apply pending migrations to a database, generate deployable SQL scripts, and safely roll back schema changes.
Handling Schema Drift in Teams
Strategies for detecting and resolving mismatches between the EF Core model, migration history, and the actual database schema across a team.
Migrations Fundamentals
Learn how EF Core migrations track model changes and generate versioned database schema updates while preserving existing data.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics