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:
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:
- Execution layer: Kubernetes Jobs or Helm hooks that run once, fail fast, and never hold your workloads hostage.
- 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.