100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Migrations in CI/CD Pipelines

Patterns for safely automating EF Core migration deployment within continuous integration and delivery pipelines, including zero-downtime strategies.

MigrationsAdvanced9 min readJul 10, 2026
Analogies

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.

yaml
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

Was this page helpful?

Topics covered

#EntityFrameworkCoreStudyNotes#MicrosoftTechnologies#MigrationsInCICDPipelines#Migrations#Pipelines#Not#Migrate#DevOps#StudyNotes#SkillVeris