Client Area

Spring Boot + PostgreSQL + Flyway: Database Migration Workflow on DomainIndia VPS

ByDomain India Team·DomainIndia Engineering
6 min readPublished 24 Apr 2026Updated 23 Jun 2026436 views

In this article

  • 1Why you need migrations
  • 2Flyway vs Liquibase
  • 3Step 1 — Add to Spring Boot
  • 4Step 2 — Migration file structure
  • 5Step 3 — Example migrations

Spring Boot + PostgreSQL + Flyway: Database Migration Workflow on DomainIndia VPS

TL;DR
Flyway (or Liquibase) version-controls your database schema changes so every environment — dev, staging, production — ends up with exactly the same DDL. This guide covers setup, migration file conventions, rollback strategies, and production deployment patterns for a Spring Boot app on DomainIndia VPS.

Why you need migrations

Without a migration tool, schema changes are manual. Symptoms:

  • "I forgot to run that ALTER TABLE on staging"
  • Dev DB has columns production doesn't (or vice versa)
  • Rolling back a release doesn't roll back the DB
  • New team member's local DB is half-set-up

With Flyway:

  • Every schema change is an SQL file committed to git
  • flyway migrate replays missing changes in order
  • flyway info shows which migrations ran on each DB
  • DB schema versioned alongside app code

Flyway vs Liquibase

ToolFormatMaturityBest for
FlywayPlain SQL (+ Java-based)Mature, simplerMost Spring Boot apps
LiquibaseXML/YAML/SQLOlder, more featuresCross-DB projects, complex rollbacks

Pick Flyway for most Spring Boot apps. Plain SQL is readable, reviewable, standard.

Step 1 — Add to Spring Boot

pom.xml:

xml
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-database-postgresql</artifactId>
</dependency>

Spring Boot auto-configures Flyway — it runs migrations on startup.

application.yml:

yaml
spring:
  datasource:
    url: jdbc:postgresql://localhost/myapp
    username: spring
    password: ${DB_PASSWORD}
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true
    validate-on-migrate: true

Step 2 — Migration file structure

Create src/main/resources/db/migration/:

V1__create_users.sql
V2__create_orders.sql
V3__add_user_phone.sql
V4__seed_default_roles.sql
V5__index_orders_created_at.sql

Naming:

  • V (uppercase) prefix
  • Version number (integer or dotted: V1, V1.1, V2)
  • __ (two underscores)
  • Description (letters, numbers, underscores)
  • .sql extension

Never edit a migration after it's run. Always add a new migration. Flyway tracks applied migrations + their checksums.

Step 3 — Example migrations

V1__create_users.sql:

sql
CREATE TABLE users (
    id         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    email      text UNIQUE NOT NULL,
    password_hash text NOT NULL,
    name       text NOT NULL,
    created_at timestamptz DEFAULT now(),
    updated_at timestamptz DEFAULT now()
);

CREATE INDEX idx_users_email ON users(email);

V3__add_user_phone.sql:

sql
ALTER TABLE users ADD COLUMN phone text;
CREATE INDEX idx_users_phone ON users(phone) WHERE phone IS NOT NULL;
Info

Partial indexes (WHERE phone IS NOT NULL) are a Postgres gem — smaller index, faster writes. Use them for sparse columns.

Step 4 — Running migrations

In dev: Spring Boot runs them on app startup automatically.

In production: disable auto-run in app (spring.flyway.enabled: false) and run as a separate step in your deploy:

bash
./mvnw flyway:migrate -Dflyway.url=$DATABASE_URL -Dflyway.user=$DB_USER -Dflyway.password=$DB_PASSWORD

Or via the Flyway CLI:

bash
flyway -url=$DATABASE_URL -user=$DB_USER -password=$DB_PASSWORD migrate

Why separate? Web servers restart unexpectedly. Running migrations on startup means a slow migration blocks health checks → load balancer kills the app → migration restarts partway through → broken DB.

Step 5 — systemd migration unit

Create a one-shot systemd service that runs migrations once, before the app starts:

/etc/systemd/system/spring-app-migrate.service:

ini
[Unit]
Description=Flyway migrations
Before=spring-app.service
Requires=postgresql.service

[Service]
Type=oneshot
User=spring
WorkingDirectory=/opt/spring-app
ExecStart=/usr/bin/java -jar /opt/spring-app/app.jar 
    --spring.flyway.enabled=true 
    --spring.flyway.repair-on-migrate=false
EnvironmentFile=/opt/spring-app/.env

In spring-app.service:

ini
[Unit]
After=network.target postgresql.service spring-app-migrate.service
Requires=spring-app-migrate.service

Now restart: sudo systemctl restart spring-app — migrations run first, then the app starts.

Step 6 — Rollback strategy

Flyway's undo feature is Teams-tier only. For OSS, roll back via forward migrations:

sql
-- V10__drop_user_phone.sql (forward-only rollback of V3)
ALTER TABLE users DROP COLUMN phone;

Better pattern: design migrations to be backward-compatible:

  • Adding a column — always compatible. Old code ignores the new column.
  • Removing a column — do it in 2 releases:

1. Release N: stop writing to the column in code

2. Release N+1 (after verifying): drop the column via migration

  • Renaming a column — do it in 3 steps:

1. Add new column, dual-write

2. Backfill old data to new column

3. Remove old column

This way, rolling back a bad code deploy doesn't require a DB rollback.

Step 7 — Seed data

For development fixtures:

V4__seed_default_roles.sql:

sql
INSERT INTO roles (name, permissions) VALUES
    ('admin', '{"*": true}'),
    ('user',  '{"read": true}'),
    ('guest', '{}')
ON CONFLICT (name) DO NOTHING;

Use ON CONFLICT so re-running doesn't fail. Idempotent.

For test-only data, prefer Spring's @Sql test annotation or data.sql, not Flyway — test data shouldn't land in prod.

Step 8 — Repeatable migrations

For views/functions/triggers that you edit frequently, use R__ prefix:

R__update_search_view.sql:

sql
CREATE OR REPLACE VIEW search_items AS
SELECT id, name, description, created_at FROM products
UNION ALL
SELECT id, title, body, published_at FROM articles;

Flyway detects changes via checksum and re-runs. Useful for views and stored procedures.

Step 9 — CI/CD integration

GitHub Actions:

yaml
- name: Validate migrations
  run: ./mvnw flyway:validate -Dflyway.url=${{ secrets.STAGING_DB_URL }}

- name: Dry run
  run: ./mvnw flyway:info -Dflyway.url=${{ secrets.STAGING_DB_URL }}

- name: Run on staging
  if: github.ref == 'refs/heads/develop'
  run: ./mvnw flyway:migrate -Dflyway.url=${{ secrets.STAGING_DB_URL }}

- name: Run on production (manual approval)
  if: github.ref == 'refs/heads/main'
  environment: production   # requires approver in GH
  run: ./mvnw flyway:migrate -Dflyway.url=${{ secrets.PROD_DB_URL }}

Monitoring migrations

Check state:

bash
./mvnw flyway:info -Dflyway.url=$DATABASE_URL

Output:

+------------+---------+---------------+---------+---------------------+----------+
| Category   | Version | Description   | Type    | Installed On        | State    |
+------------+---------+---------------+---------+---------------------+----------+
| Versioned  | 1       | create users  | SQL     | 2026-01-15 10:00:00 | Success  |
| Versioned  | 2       | create orders | SQL     | 2026-01-16 11:00:00 | Success  |
| Versioned  | 3       | add user phone| SQL     | 2026-02-01 14:00:00 | Success  |
| Versioned  | 4       | seed roles    | SQL     |                     | Pending  |
+------------+---------+---------------+---------+---------------------+----------+

Common pitfalls

FAQ

Q Flyway or Liquibase?

Flyway for most Spring Boot apps — simpler, plain SQL. Liquibase if you need cross-DB abstraction (same XML runs on Postgres and Oracle), or first-class rollback support.

Q Can I run Flyway outside Spring?

Yes — Flyway has a CLI, Docker image, Gradle plugin, Maven plugin. Common in CI/CD pipelines where you don't want the JVM overhead.

Q Does Flyway work with MySQL / MariaDB?

Yes — swap flyway-database-postgresql for flyway-database-mysql. SQL syntax differences between DBs are up to you.

Q How big can a migration file be?

Keep under 1 MB. Huge seed data should be in separate files or loaded via COPY FROM + CSV.

Q Can I use Flyway on DomainIndia shared hosting?

Depends — shared hosting with Node.js apps uses npm node-pg-migrate. For Spring Boot specifically, you need VPS (Java can't run on shared).

Production Spring Boot + Flyway needs a solid VPS with PostgreSQL. View VPS plans

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket