Engineering

Database Schema Migration Simplified: Kubernetes and Beyond

May 12, 2025
Avadhesh Karia
Neel Punatar

Database schema migrations are essential for managing relational databases as applications grow and their data needs change. Database schema migration involves two main parts: migration execution approach and choosing a migration tool.

In Kubernetes, how you run database migrations can directly impact your deployment stability. Will your app scale while a migration is in progress, or will it stall due to locks? This post breaks down proven strategies to execute migrations safely and predictably in containerised environments.

Understanding Database Schema Migrations

As applications grow, their database schemas need changes such as adding new tables, modifying columns, or changing relationships. Manually managing these changes can be difficult and error-prone, especially with multiple collaborators. Database schema migration tools automate these tasks, ensuring consistency across environments.

Database Migration Execution Approaches

Migration execution approaches vary based on your deployment platform:

Application Architecture Migration Execution Approach
Containerized on Kubernetes Kubernetes Job, Helm Hook (pre-install/upgrade), Pre-deploy job
Monolith (Single Service App) Application startup: Runs migrations automatically when the app starts; ensure only one instance executes migrations to avoid conflicts.
Self-managed VM Clusters Execute migrations using SSH, cron, or automation tools like Ansible; use systemd or init scripts before starting the application.
Heroku Use the release phase command in the Procfile or a manual script via Heroku CLI.
AWS App Runner / Cloud Run A separate migration job via CI/CD is recommended; avoid running migrations in the container entrypoint.
Serverless (Lambda/FaaS) Use a separate CI/CD job or CLI tool for migrations; avoid embedding migrations in cold-start processes.
PaaS with built-in DB Use CLI migration runners and integration with GitHub Actions or external CI pipelines.
Vercel / Netlify Not suitable for backend migrations; use external backend services or serverless functions to handle database changes.

Integrating Migrations into Kubernetes

Deploying applications on Kubernetes brings challenges such as ensuring database migrations complete before new application versions start, preventing concurrent migrations, and allowing application pods to scale during migrations. Additionally, database security must be maintained by avoiding public exposure and securely handling credentials.

1. Migrations at Application Startup

Running migrations at application startup can cause:

  • Concurrency Issues: Multiple pods might run migrations simultaneously.
  • Startup Delays: Slow migrations delay the application's readiness.
  • Separation or concerns: Migration and application code are not separated.
for attempt in range(MIGRATION_MAX_RETRIES):
    try:
        acquire_migration_lock(engine)
        run_migrations()
        print("Database Schema Migration completed.")
        return
    except OperationalError:
        print(f"Attempt {attempt + 1}: Lock acquisition failed, retrying in {MIGRATION_RETRY_DELAY}s...")
        time.sleep(MIGRATION_RETRY_DELAY)
    except Exception as e:
        print(f"Migration failed: {e}")
        sys.exit(1)

** Locking prevents concurrent migrations, but it also blocks scaling during the process, which limits the benefits of running in a Kubernetes environment.

2. Init Containers

Init containers run setup tasks before main containers but face issues:

  • Redundant Execution: Each pod runs migrations separately.
  • Complex Coordination: Additional measures are needed to manage concurrency.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      initContainers:
        - name: db-migrate
          image: migrate/migrate:v4.15.2
          command: ["sh", "-c"]
          args:
            - >
              echo "Running DB migration...";
              ./migrate -path=/migrations -database=${DB_URL} up
          env:
            - name: DB_URL
              valueFrom:
                secretKeyRef:
                  name: my-db-secret
                  key: url
      containers:
        - name: my-app
          image: my-app:latest
          ports:
            - containerPort: 8080

** While init containers separate migration logic, coordinating migrations during startup can hinder scaling.

3. Kubernetes Jobs

Kubernetes Jobs offer better control:

  • Single Execution: Jobs execute migrations once, avoiding duplication.
  • Clear Separation: Migration logic is separate from the application code.

However, managing job execution timing requires extra setup; Helm addresses this need efficiently.

4. Helm Hooks

Helm hooks execute migrations at specific lifecycle stages (pre-install and pre-upgrade), providing:

  • Ordered Execution: Migrations run before application pods
  • Clean Lifecycle Management: Deployment halts if migrations fail.
  • Scaling Flexibility: Application pods scale independently of migrations.

Example of a Helm-managed secret created before the migration job:

Helm job definition:

apiVersion: batch/v1
kind: Job
metadata:
  name: predeploy-my-app
  namespace: my-app
  labels:
    app.kubernetes.io/name: my-app
  annotations:
    helm.sh/hook: "pre-install, pre-upgrade"
    helm.sh/hook-delete-policy: "before-hook-creation"
    helm.sh/hook-weight: "2"
spec:
  backoffLimit: 0
  activeDeadlineSeconds: 480
  template:
    spec:
      restartPolicy: Never
      imagePullSecrets:
        - name: regcred
      containers:
        - name: pre-deploy
          image: docker.io/my-org/my-app:12.34
          imagePullPolicy: IfNotPresent
          command: ["yarn", "migrate"]
          env:
            - name: NODE_ENV
              value: "staging"
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: my-app-secrets
                  key: db_password

Simplifying Database Schema Migrations with Kapstan

  • Kapstan simplifies migrations in Kubernetes:
  • No YAML Needed: Easy-to-use interface.
  • Automated Migrations: Runs migrations automatically pre-deployment.
  • Error Handling: Halts deployment if migrations fail, ensuring reliability.

Kapstan clearly separates application logic from database management.

Database Schema Migrations Conclusion

Schema migrations can be the silent hero—or the silent blocker—of every release. The safest path combines two things you’ve seen in this post:

  1. Execution layer: Kubernetes Jobs or Helm hooks that run once, fail fast, and never hold your workloads hostage.
  2. Tooling layer: A migration tool that matches your team’s workflow and governance needs—Flyway, Liquibase, Atlas, golang-migrate, or whichever fits your stack.

When those layers work together, deployments stay green, pods keep scaling, and the database evolves without drama.

Kapstan takes that proven recipe and bakes it into the platform:

  • One-click migration jobs generated for every release—no YAML wrangling.
  • Built-in secrets management and rollback safety nets.
  • Clear logs and alerts so you know exactly when—and why—something fails.

If you’re

✅ Evaluating Kubernetes for the first time

✅ Juggling multiple services, or

✅ Just tired of late-night migration mishaps, we’d love to help. Drop us a note or book a quick call, and our architects will walk through your current setup and map out a zero-surprise migration strategy.

Ready to turn database changes from stress into routine? Let’s chat.

Avadhesh Karia
CoFounder & Chief Architect @ Kapstan. Avadhesh has been passionate about tackling productivity bottlenecks for developers for over two decades, enhancing efficiency and innovation.

Simplify your DevEx with a single platform

Schedule a demo