Client Area

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

ByDomain India Team·DomainIndia Engineering
6 min read24 Apr 20265 views
# 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 org.flywaydb flyway-core org.flywaydb flyway-database-postgresql ``` 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

Still need help?

Our support team can assist you directly.

Submit Ticket