DevOps & CI/CD14 min read·By Liyabona Saki·

Automating Database Migrations with Flyway and Spring Boot in a CI/CD Pipeline

Ship safe, versioned, zero-downtime database migrations with Flyway and Spring Boot — including PostgreSQL examples, multi-environment handling and a complete GitHub Actions pipeline.

Advertisement

Introduction

Production outages caused by *database* changes outnumber outages caused by *code* changes in most teams I've worked with. Flyway brings the same discipline to schema changes that Git brings to source: versioned, repeatable, auditable, and automated through CI/CD.

Why database migrations matter

  • Reproducibility — every environment is built from the same ordered scripts.
  • Auditabilityflyway_schema_history is a permanent record.
  • Safety — every script runs exactly once, in order, in a transaction.
  • Automation — schema changes ship with the code that needs them.

Step 1 — Add Flyway to Spring Boot

xml
<dependency>
  <groupId>org.flywaydb</groupId>
  <artifactId>flyway-core</artifactId>
</dependency>
<dependency>
  <groupId>org.flywaydb</groupId>
  <artifactId>flyway-database-postgresql</artifactId>
</dependency>
yaml
spring:
  datasource:
    url: jdbc:postgresql://db:5432/orders
    username: app
    password: ${DB_PASSWORD}
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true
    validate-on-migrate: true

Flyway runs automatically on application startup.

Step 2 — Folder structure

text
src/main/resources/db/migration/
  V1__init_schema.sql
  V2__add_orders_status_index.sql
  V3__add_customers_email_unique.sql
  R__refresh_reporting_views.sql
  • V<version>__description.sql — versioned, applied once, in order.
  • R__<name>.sql — repeatable, re-applied whenever the file's checksum changes (good for views).
  • U<version>__ — undo migrations (paid tier only — don't rely on them; design forward).

Step 3 — A migration

```sql
-- V1__init_schema.sql
CREATE TABLE customers (
  id BIGSERIAL PRIMARY KEY,
  email TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, customer_id BIGINT NOT NULL REFERENCES customers(id), total_cents BIGINT NOT NULL, status TEXT NOT NULL DEFAULT 'PENDING', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ```

sql
-- V2__add_orders_status_index.sql
CREATE INDEX CONCURRENTLY idx_orders_status ON orders(status);

CONCURRENTLY avoids locking the table — essential for production. It also means the statement can't run in a transaction; configure that script with -- flyway:executeInTransaction=false.

Step 4 — Zero-downtime migration pattern

Never combine schema + code change in one deploy. Use expand / migrate / contract:

1. Expand — add the new column/table. Deploy. Code still uses the old shape. 2. Migrate — backfill data. Deploy code that reads/writes *both* shapes. 3. Contract — drop the old column. Deploy.

Example: renaming orders.total_cents to orders.amount_cents:

sql
-- V10__add_amount_cents.sql
ALTER TABLE orders ADD COLUMN amount_cents BIGINT;
UPDATE orders SET amount_cents = total_cents WHERE amount_cents IS NULL;
sql
-- V12__drop_total_cents.sql  (deployed AFTER all instances read amount_cents)
ALTER TABLE orders DROP COLUMN total_cents;

Step 5 — Multi-environment handling

```yaml
# application-dev.yml
spring:
  flyway:
    locations: classpath:db/migration,classpath:db/dev-seed

# application-prod.yml spring: flyway: locations: classpath:db/migration clean-disabled: true # CRITICAL — prevents flyway:clean wiping prod ```

Always set clean-disabled: true for non-dev environments.

Step 6 — Dockerized local Postgres

yaml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: orders
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
    ports: ["5432:5432"]

docker compose up then ./mvnw spring-boot:run — Flyway migrates the schema on boot.

Step 7 — GitHub Actions pipeline

```yaml
name: ci
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_DB: orders, POSTGRES_USER: app, POSTGRES_PASSWORD: app }
        ports: ["5432:5432"]
        options: >-
          --health-cmd pg_isready --health-interval 5s --health-retries 10
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: 21, cache: maven }
      - name: Validate migrations
        run: ./mvnw -B flyway:validate
      - name: Apply migrations (CI db)
        run: ./mvnw -B flyway:migrate
      - name: Tests
        run: ./mvnw -B test

deploy-prod: needs: build if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: { distribution: temurin, java-version: 21 } - name: Migrate production DB env: FLYWAY_URL: ${{ secrets.PROD_DB_URL }} FLYWAY_USER: ${{ secrets.PROD_DB_USER }} FLYWAY_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }} run: ./mvnw -B flyway:migrate - name: Deploy application run: ./deploy.sh ```

Note: migrations run *before* the new application version is deployed, so the schema is always at least as new as the running code.

Common migration mistakes

  • Editing an applied V-script — Flyway detects the checksum mismatch and refuses to start. Never edit history; add a new script.
  • Long-running ALTER TABLE in business hours — locks the table. Use CONCURRENTLY, batched updates, or run during a maintenance window.
  • Mixing DDL and DML in one script on MySQL — DDL auto-commits, so a failure halfway leaves you in a half-applied state.
  • Forgetting clean-disabled — one CLI mistake wipes prod.

Production best practices

  • Use a dedicated migration user with only CREATE/ALTER rights — not the app user.
  • Run flyway:migrate from CI/CD, not from the application on startup, for high-replica deployments.
  • Keep migrations small — one logical change per V-script.
  • Add a -- @author / @ticket header so the audit trail is meaningful.

Related tutorials

Architecture

Flyway Migrations in CI/CD

SOURCECIENVIRONMENTSDATABASEpushpromotemigratemigrateRepoV1__init.sql · V2__add_index.sqlCI Pipelineflyway migrateStaging AppProduction AppStaging DBProd DB
Versioned SQL files live with the app. CI runs migrate on startup against staging then production, recording each version in flyway_schema_history.

TL;DR

Key takeaways

  • Understand the core concepts behind Automating Database Migrations with Flyway and Spring Boot in a CI/CD Pipeline in a production context.
  • Apply the patterns to real DevOps & CI/CD systems, not just toy examples.
  • Recognize the trade-offs, failure modes, and operational concerns before adopting them.
  • Get a clear path to the next step — related tutorials, tools, and reference architectures.

Avoid these

Common mistakes

  • 1. Copy-pasting code without understanding the trade-offs

    It's tempting to ship a snippet from a blog post into production, but DevOps & CI/CD patterns only work when the failure modes are understood. Always reason about timeouts, retries, and consistency.

  • 2. Skipping observability from day one

    Structured logs, metrics, and traces are not optional. Wire them in before you ship — debugging DevOps & CI/CD systems without them is painful and expensive.

  • 3. Optimizing too early

    Premature caching, sharding, or microservice extraction adds operational cost. Validate the bottleneck with real measurements first.

  • 4. Ignoring security defaults

    Secrets in env files, open management ports, missing RBAC — these are the most common production incidents. Treat security as part of the definition of done.

Ship it safely

Production best practices

Apply these before promoting Automating Database Migrations with Flyway and Spring Boot in a CI/CD Pipeline to a real production environment.

Scalability

Design DevOps & CI/CD services to scale horizontally. Keep request handlers stateless, push session and cache state to external stores (Redis, the database), and benchmark p95/p99 latency under realistic load before tuning.

Monitoring & Observability

Emit metrics (RED/USE), structured JSON logs, and distributed traces from day one. Wire dashboards and alerts to SLOs you actually care about — error rate, latency, saturation — not vanity metrics.

Logging

Log with correlation IDs, never log secrets or PII, and centralize logs (ELK, Loki, CloudWatch). Use levels deliberately: INFO for state changes, WARN for recoverable issues, ERROR for incidents.

Security

Apply least-privilege IAM, rotate secrets through a vault, validate every input, and patch dependencies on a schedule. For HTTP services, enable TLS everywhere and set sensible security headers.

Testing

Layer unit, integration, and contract tests. Run them in CI on every PR, and add smoke tests post-deploy. For DevOps & CI/CD systems, also run chaos and load tests before a major release.

Reliability & Rollouts

Ship with health checks, readiness probes, graceful shutdown, and a rollback strategy. Prefer canary or blue/green deploys over big-bang releases.

Questions

Frequently asked questions

Is this tutorial up to date?

Yes. This tutorial was last reviewed and updated on May 22, 2026. We revisit popular DevOps & CI/CD tutorials regularly to keep them aligned with current best practices.

What level is this tutorial aimed at?

It is written for working developers with some backend experience. Beginners can still follow along, and senior engineers will find production-grade patterns and trade-off discussions.

Do I need to follow every step in order?

The walkthrough is sequential because each step depends on the previous one. If you only need a specific concept, the table of contents at the top of the article lets you jump straight to that section.

Where can I find the source code?

Code samples are inlined in the tutorial. When a companion repository is published it will be linked at the top of this page.

Go deeper

Further reading

#Flyway#Spring Boot#PostgreSQL#CI/CD#GitHub Actions#Database

More From the Channel

Follow the full tutorial series on YouTube

The MasterLabSystems channel publishes in-depth, project-based tutorials on Java, Spring Boot, microservices, Docker, Kubernetes, AWS and DevOps — the same topics covered on this site, with full code walkthroughs.

Stay in the Loop

Get the next tutorial in your inbox

next tutorial →

Serverless Java: Spring Boot on AWS Lambda with GraalVM

Related tutorials