Skip to content

CI/CD for Migrations — Complete Guide

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about CI/CD for Migrations. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn CI/CD integration for database migrations: automate Migration execution in deployment pipelines, test migrations in CI, manage migration state across environments, and implement safe deployment strategies with automated rollback on failure.

What You Learn

You will learn how to integrate database migrations into CI/CD pipelines: running migrations as a pipeline stage, testing migrations in CI with fresh databases, managing migration state across dev/staging/production environments, implementing safe deployment strategies with automated rollback, and monitoring migration execution in production.

Why It Matters

Manual migration execution is error-prone and slow. Developers forget to run migrations before deploying. Production schema drifts from staging. Rollbacks require manual commands under pressure. Automating migrations in CI/CD ensures consistent, safe, and auditable schema changes across all environments.

Real-World Use

DodaTech runs migrations as an automated stage in GitLab CI. The pipeline creates a fresh database, applies all migrations, runs the test suite, and verifies rollback. When a migration fails in production, the pipeline automatically rolls back and notifies the team. Zero manual migration execution in the last 18 months.

Pipeline Architecture

graph LR
    Commit[Code Commit] --> CI[CI Pipeline]
    CI --> Build[Build Stage]
    CI --> MigrationTest[Migration Test]
    CI --> UnitTest[Unit Tests]
    Build --> Deploy[Deploy Stage]
    MigrationTest --> Deploy
    UnitTest --> Deploy
    Deploy --> Migrate[Run Migrations]
    Deploy --> AppDeploy[Deploy Application]
    Migrate --> Verify[Health Check]
    AppDeploy --> Verify
    Verify --> Success[Deploy Success]
    Verify --> Rollback[Auto Rollback]

The CI/CD pipeline has two main phases: CI (test migrations) and CD (execute migrations and deploy). Migration testing in CI catches issues before they reach production.

CI Pipeline: Migration Testing

# .github/workflows/ci-migrations.yml
name: CI - Migration Tests

on:
    pull_request:
        paths:
            - 'migrations/**'
            - 'models/**'
            - 'prisma/schema.prisma'

jobs:
    migration-tests:
        runs-on: ubuntu-latest
        services:
            postgres:
                image: postgres:16
                env:
                    POSTGRES_PASSWORD: postgres
                ports:
                    - 5432:5432
                options: >-
                    --health-cmd pg_isready
                    --health-interval 10s
                    --health-timeout 5s
                    --health-retries 5

        steps:
            - uses: actions/checkout@v4

            - name: Set up Python
              uses: actions/setup-python@v5
              with:
                  python-version: '3.12'

            - name: Install dependencies
              run: pip install -r requirements-dev.txt

            - name: Create test database
              run: createdb -h localhost -U postgres ci_test

            - name: Apply all migrations from scratch
              run: alembic upgrade head
              env:
                  DATABASE_URL: postgresql://postgres:postgres@localhost/ci_test

            - name: Verify schema
              run: python -m scripts.verify_schema
              env:
                  DATABASE_URL: postgresql://postgres:postgres@localhost/ci_test

            - name: Test rollback
              run: |
                  alembic downgrade -1
                  alembic upgrade head
              env:
                  DATABASE_URL: postgresql://postgres:postgres@localhost/ci_test

            - name: Run application tests
              run: pytest tests/
              env:
                  DATABASE_URL: postgresql://postgres:postgres@localhost/ci_test

Expected output: CI pipeline runs migration tests on every PR that changes migrations or models. It creates a fresh database, applies all migrations, verifies schema, tests rollback, and runs the application test suite.

CD Pipeline: Migration Execution

# .gitlab-ci.yml with migration stages
stages:
  - build
  - migrate
  - deploy
  - verify

variables:
  MIGRATION_IMAGE: $CI_REGISTRY_IMAGE/migration:$CI_COMMIT_SHA

build-migration-image:
  stage: build
  script:
    - docker build -t $MIGRATION_IMAGE -f Dockerfile.migration .
    - docker push $MIGRATION_IMAGE
  only:
    - main

run-migrations:
  stage: migrate
  image: $MIGRATION_IMAGE
  script:
    - echo "Running migrations..."
    - alembic upgrade head
    - echo "Migrations completed"
  environment:
    name: production
  only:
    - main
  when: manual  # Require manual approval for production migrations
  allow_failure: false

deploy-application:
  stage: deploy
  script:
    - kubectl set image deployment/app app=$CI_REGISTRY_IMAGE/app:$CI_COMMIT_SHA
    - kubectl rollout status deployment/app
  environment:
    name: production
  needs: ["run-migrations"]
  only:
    - main

verify-deployment:
  stage: verify
  script:
    - python scripts/health_check.py
    - python scripts/verify_schema.py
    - python scripts/smoke_tests.py
  environment:
    name: production
  needs: ["deploy-application"]
  only:
    - main

auto-rollback:
  stage: verify
  script:
    - python scripts/rollback.py
    - kubectl rollout undo deployment/app
  environment:
    name: production
  needs: ["deploy-application"]
  when: on_failure  # Triggered when verify fails
  only:
    - main

Expected output: CD pipeline runs migrations as a separate stage before deployment. If migrations fail, the deployment is skipped. If verification fails after deployment, automated rollback reverts both schema and application.

Multi-Environment Migration Strategy

# Environment-specific migration configuration
# .env.development
DATABASE_URL=postgresql://dev:dev@localhost/dev_db
AUTO_MIGRATE=true  # Auto-run migrations on dev startup

# .env.staging
DATABASE_URL=postgresql://staging:staging@staging-host/staging_db
AUTO_MIGRATE=true  # Auto-run migrations in staging CI

# .env.production
DATABASE_URL=postgresql://prod:prod@prod-host/prod_db
AUTO_MIGRATE=false  # Require manual approval for production
#!/bin/bash
# scripts/run_migrations.sh
set -euo pipefail

ENVIRONMENT=${1:-development}
echo "Running migrations for $ENVIRONMENT environment..."

case $ENVIRONMENT in
    development)
        # Auto-run migrations on dev startup
        alembic upgrade head
        echo "Development migrations applied"
        ;;

    staging)
        # Auto-run in staging CI/CD
        alembic upgrade head
        echo "Staging migrations applied"
        ;;

    production)
        # Require confirmation for production
        echo "WARNING: Running migrations on PRODUCTION"
        echo "Current migration state:"
        alembic current

        read -p "Continue with production migration? (yes/no): " confirmation
        if [ "$confirmation" != "yes" ]; then
            echo "Migration cancelled"
            exit 1
        fi

        # Take a backup before migrating
        echo "Taking database backup..."
        pg_dump $DATABASE_URL > "backup_$(date +%Y%m%d_%H%M%S).sql"

        # Run migrations
        alembic upgrade head
        echo "Production migrations applied"

        # Verify
        python scripts/verify_schema.py
        echo "Schema verification passed"
        ;;
esac

Expected output: Migration script adapts to each environment. Development and staging auto-run migrations. Production requires manual confirmation and takes a backup before migrating.

Migration Status Monitoring

# scripts/monitor_migrations.py
"""Monitor migration execution and report status."""
import os
import time
import requests
from datetime import datetime

def monitor_migration():
    """Monitor migration execution and report to team."""
    webhook_url = os.environ.get('SLACK_WEBHOOK_URL')
    start_time = time.time()

    def report(message, status='info'):
        if webhook_url:
            requests.post(webhook_url, json={
                'text': f'[Migration] {message}',
                'attachments': [{
                    'color': {
                        'info': '#3498db',
                        'success': '#2ecc71',
                        'error': '#e74c3c',
                    }.get(status, '#3498db'),
                    'fields': [
                        {'title': 'Environment', 'value': os.environ.get('ENV', 'unknown'), 'short': True},
                        {'title': 'Timestamp', 'value': datetime.now().isoformat(), 'short': True},
                        {'title': 'Duration', 'value': f'{time.time() - start_time:.1f}s', 'short': True},
                    ]
                }]
            })

    try:
        report('Migration starting...')

        # Run migration
        result = subprocess.run(
            ['alembic', 'upgrade', 'head'],
            capture_output=True, text=True, check=True,
            timeout=300
        )

        report(f'Migration completed in {time.time() - start_time:.1f}s', 'success')
        return True

    except subprocess.CalledProcessError as e:
        report(f'Migration FAILED: {e.stderr[:500]}', 'error')
        return False
    except subprocess.TimeoutExpired:
        report('Migration TIMEOUT after 5 minutes', 'error')
        return False

Expected output: Migration monitor reports start, completion, and failure to the team chat. Includes environment, timestamp, and duration. Failures trigger alerts for immediate investigation.

Safe Deployment Strategies

# Deployment strategies for different migration risk levels

# Strategy 1: Blue-Green Deployment (safe for most migrations)
blue-green:
  - Run migrations on both blue and green databases
  - Switch traffic after both are migrated
  - Rollback: switch traffic back

# Strategy 2: Expand-Contract Pattern (for breaking changes)
expand-contract:
  - Phase 1: Add new columns/tables (non-breaking)
  - Phase 2: Update application to use new schema
  - Phase 3: Remove old columns/tables (in a later release)
  - Rollback: Revert application code, keep old schema

# Strategy 3: Feature Flags
feature-flags:
  - Add column with feature flag gating
  - Deploy with flag disabled
  - Enable flag after verifying migration
  - Rollback: Disable flag
# scripts/expand_contract_migration.py
"""Example of expand-contract pattern."""
def phase1_expand():
    """Add new columns alongside old ones."""
    op.add_column('users', sa.Column('email_new', sa.String(255)))
    op.add_column('users', sa.Column('email_verified_new', sa.Boolean()))

def phase2_migrate_data():
    """Backfill new columns from old ones."""
    op.execute("""
        UPDATE users
        SET email_new = email,
            email_verified_new = email_verified
    """)

def phase3_contract():
    """Remove old columns (in a future release)."""
    op.drop_column('users', 'email')
    op.drop_column('users', 'email_verified')
    op.rename_column('users', 'email_new', 'email')
    op.rename_column('users', 'email_verified_new', 'email_verified')

Expected output: Expand-contract pattern adds new columns without removing old ones. The application is updated to use new columns. Old columns are removed in a later release. This allows safe rollback at any phase.

Common Mistakes

1. Running Migrations in the Same Stage as Application Deploy

Migrations and application deploy should be separate stages. If the deploy fails after migration, you need a rollback. Separate stages allow independent failure handling.

2. No Manual Approval Gate for Production

Auto-running migrations on production without approval is risky. Require manual approval before production migration execution. Use environment protection rules in CI/CD.

3. Not Testing Migrations in CI

Merging migration PRs without testing in CI causes production failures. Run migration tests in CI: apply from scratch, verify schema, test rollback. Gate the merge on test pass.

4. Ignoring Migration Failures in Pipeline

A failed migration that is ignored leaves the database in an unknown state. Fail the pipeline on migration failure. Do not proceed with deployment. Investigate and fix before retrying.

5. No Rollback Automation

A deployment pipeline without automated rollback requires manual intervention during incidents. Implement automated rollback triggered by verification failure. Test the rollback path regularly.

6. Different Migration Order Across Environments

Migrations should be applied in the same order across all environments. Use the same migration tool and configuration. Verify migration state consistency across environments.

Practice Questions

1. Why should migrations be a separate stage in CI/CD?

Separate stages allow independent failure handling. If migrations fail, the deployment is skipped. If verification fails after deploy, rollback reverts both schema and application. Stages provide clear failure boundaries.

2. What is the expand-contract pattern?

A safe deployment strategy for breaking schema changes. Phase 1 adds new columns alongside old ones. Phase 2 migrates data. Phase 3 removes old columns in a later release. Rollback is possible at any phase.

3. When should you require manual approval for migrations?

Production migrations should require manual approval. Development and staging can auto-run. Manual approval prevents accidental production schema changes and ensures a human reviews the migration plan.

4. How do you verify migration success after deployment?

Run health checks that verify schema correctness. Check that expected tables, columns, and constraints exist. Run smoke tests that exercise the new schema. Verify application starts without errors.

Challenge

Build a complete CI/CD migration pipeline that: runs migration tests in CI on every PR (fresh database, apply, verify, rollback), deploys with separate migrate/deploy/verify stages, requires manual approval for production migrations, takes a database backup before production migration, runs health checks after migration, triggers automated rollback on verification failure, and notifies the team via Slack with migration status.

FAQ

Can I run migrations in parallel with the application?

Not safely. The application expects a specific schema version. Running migrations while the application is running causes errors if the schema changes under it. Stop the application, run migrations, restart.

How do I handle migrations in zero-downtime deployments?

Use expand-contract pattern or online schema change tools. Add columns as nullable first, backfill data, then enforce constraints. Deploy in phases: schema changes in one release, app changes in the next.

Should I run migrations before or after application deploy?

Run migrations before application deploy. The new application code expects the new schema. If migrations fail, the old application continues running with the old schema.

How do I handle migrations across multiple regions?

Run migrations in one region first. Verify the migration succeeds. Then run in other regions sequentially. If a migration fails, fix before proceeding to the next region.

Can I use the same CI/CD pipeline for different database types?

Use matrix builds to test against multiple databases. Run migration tests against PostgreSQL, MySQL, and SQLite in parallel. The deployment pipeline uses the production database type.

How do I handle emergency hotfix migrations?

Create an emergency migration with expedited review. Run CI tests (shortened). Trigger the deployment pipeline with manual approval. Monitor closely. Document the incident for post-mortem.

Mini Project: CI/CD Migration Pipeline

Build a complete CI/CD pipeline for database migrations: CI pipeline that tests migrations against a fresh database (apply all, verify schema, test rollback, run app tests), CD pipeline with separate migrate/deploy/verify stages, manual approval gate for production, automated backup before production migration, health check verification after migration, automated rollback on failure, and Slack notifications for migration status.

What's Next

Now that you understand CI/CD for migrations, learn about Zero-Downtime Migrations for schema changes that do not affect application availability.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro