⚠️ The Problem
Imagine you run your app in multiple containers/replicas (e.g. in Docker Swarm, Kubernetes, or just scaling docker-compose up --scale app=3).
If each replica runs database migrations on startup, then:
- All replicas connect to the database at the same time.
- Each one tries to apply the same schema changes (
ALTER TABLE,CREATE INDEX, etc.). - This can cause:
- Race conditions (two replicas try to apply the same migration file).
- Deadlocks/locks on the database.
- Migration failures — one succeeds, others crash with “migration already applied” or “table already exists”.
✅ Why Dedicated Migrations Are Safer
That’s why teams often:
- Run migrations in a dedicated container (one-off job in Docker or Kubernetes).
- Or use init containers (Kubernetes) or pre-deployment jobs in CI/CD pipelines.
- After migrations succeed → app replicas start up normally, reading the updated schema.
🔹 Example in Docker Compose
Bad (risky) approach:
services:
app:
image: my-app
depends_on: [db]
# App runs migrations on startup (risky if scaled to 3 replicas!)
Better approach:
services:
db:
image: postgres:15
migrate:
image: liquibase/liquibase
depends_on: [db]
command: ["update", "--url=jdbc:postgresql://db:5432/app", "--changelog-file=changelog.xml"]
app:
image: my-app
depends_on: [migrate] # starts only after migrations succeed
Here only one migration container runs → safe schema update. Then your app replicas can scale freely.