Every Spring Boot app that talks to PostgreSQL eventually needs a safe way to change its schema. This guide shows how to do that with Flyway, from the first migration file to a production deploy on a Linux VPS.
Flyway (or Liquibase) version-controls your database schema changes so every environment — dev, staging, production — ends up with exactly the same DDL. Write each change as a numbered SQL file, never edit one that has already run, and in production run migrations as a separate step before the app starts. Roll back with forward migrations and backward-compatible changes. This guide covers setup, file conventions, rollback strategies and deployment on a Domain India 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 migratereplays missing changes in orderflyway infoshows which migrations ran on each DB- DB schema versioned alongside app code
Flyway vs Liquibase
| Tool | Format | Maturity | Best for |
|---|---|---|---|
| Flyway | Plain SQL (+ Java-based) | Mature, simpler | Most Spring Boot apps |
| Liquibase | XML/YAML/SQL | Older, more features | Cross-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:
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>Spring Boot manages the Flyway version and auto-configures it, so migrations run on startup. Since Flyway 10, each database needs its own module, which is why flyway-database-postgresql is listed.
To run Flyway from Maven as well (used in the steps below), add the plugin with the same database module:
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
<version>${flyway.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>
</dependencies>
</plugin>application.yml:
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: truebaseline-on-migrate is only needed when you add Flyway to a database that already has tables. For a new database, leave it out.
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.sqlNaming:
V(uppercase) prefix- Version number (integer or dotted:
V1,V1.1,V2) __(two underscores)- Description (letters, numbers, underscores)
.sqlextension
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:
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()
);
The UNIQUE constraint already creates an index on email, so you don't need a second one.
V3__add_user_phone.sql:
ALTER TABLE users ADD COLUMN phone text;
CREATE INDEX idx_users_phone ON users(phone) WHERE phone IS NOT NULL;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:
./mvnw flyway:migrate -Dflyway.url=$DATABASE_URL -Dflyway.user=$DB_USER -Dflyway.password=$DB_PASSWORDOr via the Flyway CLI:
flyway -url=$DATABASE_URL -user=$DB_USER -password=$DB_PASSWORD migrateWhy 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
On your own VPS, create a one-shot systemd service that runs the Flyway CLI once, before the app starts. Copy your migration files to the server with each release (for example to /opt/spring-app/migrations).
/etc/systemd/system/spring-app-migrate.service:
[Unit]
Description=Flyway migrations
Before=spring-app.service
After=postgresql.service
Requires=postgresql.service
[Service]
Type=oneshot
User=spring
WorkingDirectory=/opt/spring-app
EnvironmentFile=/opt/spring-app/flyway.env
ExecStart=/opt/flyway/flyway -locations=filesystem:/opt/spring-app/migrations migrate/opt/spring-app/flyway.env (readable only by the spring user, chmod 600):
FLYWAY_URL=jdbc:postgresql://localhost/myapp
FLYWAY_USER=spring
FLYWAY_PASSWORD=change-meKeep spring.flyway.enabled=false in the app's production config so the app itself never migrates.
In spring-app.service:
[Unit]
After=network.target postgresql.service spring-app-migrate.service
Requires=spring-app-migrate.serviceNow restart: sudo systemctl restart spring-app — migrations run first, then the app starts.
Step 6 — Rollback strategy
Flyway's undo migrations are only in the paid editions. With the free Community edition, roll back via forward migrations:
-- 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:
- Release N: stop writing to the column in code
- Release N+1 (after verifying): drop the column via migration
- Renaming a column — do it in 3 steps:
- Add new column, dual-write
- Backfill old data to new column
- 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:
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:
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:
- 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:
./mvnw flyway:info -Dflyway.url=$DATABASE_URLOutput:
+------------+---------+---------------+---------+---------------------+----------+
| 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
NOT NULL later.V20260930_1__add_phone.sql, or renumber before pushing.pg_dump before every production migration, and keep PostgreSQL WAL archiving or point-in-time recovery set up if the data matters.Running this on Domain India
- VPS: Domain India VPS plans are self-managed KVM servers with full root access, from ₹553 a month excluding GST. You install Java, PostgreSQL and Flyway yourself, and the steps above apply as written. Snapshots and backups are not included, so schedule your own
pg_dumpbackups and copy them off the server. - App Platform: Java is not detected automatically, so deploy with your own Dockerfile. Every plan includes PostgreSQL and free SSL. There, letting Spring Boot run Flyway on startup is the simplest option while you run a single instance. There is no SSH access to the app.
- Shared hosting: not suitable for Spring Boot; see the FAQ below.
FAQ
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.
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.
Does Flyway work with MySQL / MariaDB?
Yes — swap flyway-database-postgresql for flyway-database-mysql. SQL syntax differences between DBs are up to you.
How should I load large seed data?
Keep migration files small and reviewable. Load large data sets separately, for example with PostgreSQL's COPY from a CSV file, rather than as thousands of INSERT lines in a migration.
Can I run Spring Boot and Flyway on Domain India shared hosting?
No. Shared hosting runs PHP sites and Node.js or Python apps through the panel tools; it does not run a long-running Java process. Use a Domain India VPS, where you install Java and PostgreSQL yourself, or the App Platform with your own Dockerfile.
Ready to deploy? Run Spring Boot, PostgreSQL and the Flyway unit above on a Domain India VPS, or package the app with a Dockerfile for the App Platform.
A self-managed VPS with full root access gives you Java, PostgreSQL and systemd under your control.
View VPS plans