Flyway Cheat Sheet
Overview
Flyway is a database migration tool that emphasizes simplicity and convention over configuration. It uses versioned SQL scripts (or Java-based migrations) to evolve database schemas. Flyway tracks which migrations have been applied using a flyway_schema_history table and applies pending migrations in order. It supports over 20 databases including PostgreSQL, MySQL, Oracle, SQL Server, MariaDB, and SQLite.
Flyway follows a straightforward approach: place numbered SQL files in a migrations directory, and Flyway figures out which ones need to run. It supports versioned migrations (V), repeatable migrations (R), and undo migrations (U, Teams/Enterprise). Flyway is available as a CLI tool, Maven/Gradle plugin, Java API, and Docker image.
Installation
# macOS via Homebrew
brew install flyway
# Linux
curl -L https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/10.15.0/flyway-commandline-10.15.0-linux-x64.tar.gz | tar xz
sudo ln -s $(pwd)/flyway-10.15.0/flyway /usr/local/bin/flyway
# Docker
docker pull flyway/flyway
# Verify installation
flyway --version
Build Tool Integration
<!-- Maven pom.xml -->
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>10.15.0</version>
<configuration>
<url>jdbc:postgresql://localhost:5432/mydb</url>
<user>postgres</user>
<password>password</password>
</configuration>
</plugin>
// Gradle build.gradle
plugins {
id 'org.flywaydb.flyway' version '10.15.0'
}
flyway {
url = 'jdbc:postgresql://localhost:5432/mydb'
user = 'postgres'
password = 'password'
}
Core Commands
| Command | Description |
|---|---|
flyway migrate | Apply pending migrations |
flyway info | Show migration status and history |
flyway validate | Validate applied migrations match local files |
flyway baseline | Baseline an existing database |
flyway repair | Repair the schema history table |
flyway clean | Drop all objects in the schema (dangerous) |
flyway undo | Undo the last migration (Teams/Enterprise) |
# Apply pending migrations
flyway -url=jdbc:postgresql://localhost:5432/mydb \
-user=postgres -password=password migrate
# Show migration status
flyway -url=jdbc:postgresql://localhost:5432/mydb \
-user=postgres -password=password info
# Validate migrations
flyway -url=jdbc:postgresql://localhost:5432/mydb \
-user=postgres -password=password validate
# Baseline at version 1 (for existing databases)
flyway -url=jdbc:postgresql://localhost:5432/mydb \
-user=postgres -password=password -baselineVersion=1 baseline
# Dry run (preview SQL without executing)
flyway -url=jdbc:postgresql://localhost:5432/mydb \
-user=postgres -password=password -dryRunOutput=dryrun.sql migrate
Migration File Naming
Naming Convention
V{version}__{description}.sql — Versioned migration
R__{description}.sql — Repeatable migration
U{version}__{description}.sql — Undo migration (Teams/Enterprise)
Examples:
V1__create_users_table.sql
V1.1__add_email_to_users.sql
V2__create_posts_table.sql
V2.1.3__add_index_on_posts.sql
R__create_views.sql
U2__undo_create_posts_table.sql
Versioned Migrations
-- V1__create_users_table.sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
-- V2__create_posts_table.sql
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
content TEXT,
status VARCHAR(20) DEFAULT 'draft',
user_id INTEGER NOT NULL REFERENCES users(id),
published_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_status ON posts(status);
-- V3__add_tags_system.sql
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE post_tags (
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
Repeatable Migrations
-- R__create_views.sql
-- Repeatable migrations run whenever their checksum changes
CREATE OR REPLACE VIEW active_users AS
SELECT id, email, name, created_at
FROM users
WHERE last_login > CURRENT_TIMESTAMP - INTERVAL '30 days';
CREATE OR REPLACE VIEW published_posts AS
SELECT p.id, p.title, p.content, u.name as author, p.published_at
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.status = 'published'
ORDER BY p.published_at DESC;
Configuration
flyway.conf
# flyway.conf (or flyway.toml)
flyway.url=jdbc:postgresql://localhost:5432/mydb
flyway.user=postgres
flyway.password=password
flyway.schemas=public
flyway.locations=filesystem:sql/migrations
flyway.baselineOnMigrate=true
flyway.baselineVersion=0
flyway.validateOnMigrate=true
flyway.outOfOrder=false
flyway.cleanDisabled=true
flyway.table=flyway_schema_history
flyway.placeholders.env=production
flyway.encoding=UTF-8
flyway.connectRetries=3
flyway.connectRetriesInterval=5
TOML Configuration
# flyway.toml
[flyway]
url = "jdbc:postgresql://localhost:5432/mydb"
user = "postgres"
password = "password"
locations = ["filesystem:sql/migrations"]
baselineOnMigrate = true
cleanDisabled = true
validateOnMigrate = true
[flyway.placeholders]
env = "production"
schema = "public"
Environment-Specific Configuration
# flyway-dev.conf
flyway.url=jdbc:postgresql://localhost:5432/mydb_dev
flyway.user=dev_user
flyway.password=dev_password
flyway.cleanDisabled=false
# flyway-prod.conf
flyway.url=jdbc:postgresql://prod-host:5432/mydb
flyway.user=prod_user
flyway.password=prod_password
flyway.cleanDisabled=true
# Use specific config file
flyway -configFiles=flyway-dev.conf migrate
Placeholders
-- V4__create_schema.sql
-- Use placeholders for environment-specific values
CREATE SCHEMA IF NOT EXISTS ${schema_name};
CREATE TABLE ${schema_name}.config (
key VARCHAR(100) PRIMARY KEY,
value TEXT,
environment VARCHAR(20) DEFAULT '${env}'
);
# flyway.conf
flyway.placeholders.schema_name=app
flyway.placeholders.env=production
Advanced Usage
Java-Based Migrations
// V5__populate_seed_data.java
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.Statement;
public class V5__populate_seed_data extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
try (Statement stmt = context.getConnection().createStatement()) {
stmt.execute("INSERT INTO users (email, name, password_hash) "
+ "VALUES ('admin@example.com', 'Admin', 'hashed_password')");
}
}
}
Callbacks
-- beforeMigrate.sql — runs before each migrate command
SELECT 'Starting migration at ' || CURRENT_TIMESTAMP;
-- afterMigrate.sql — runs after each migrate command
REFRESH MATERIALIZED VIEW IF EXISTS user_stats;
-- beforeEachMigrate.sql — runs before each individual migration
-- afterEachMigrate.sql — runs after each individual migration
Docker Compose
services:
db:
image: postgres:16
environment:
POSTGRES_DB: mydb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
flyway:
image: flyway/flyway
depends_on:
- db
volumes:
- ./sql/migrations:/flyway/sql
- ./flyway.conf:/flyway/conf/flyway.conf
command: migrate
CI/CD Integration
# GitHub Actions
name: Database Migration
on:
push:
branches: [main]
paths: ['sql/migrations/**']
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Flyway
run: |
docker run --rm \
-v $(pwd)/sql/migrations:/flyway/sql \
flyway/flyway \
-url="${{ secrets.DB_URL }}" \
-user="${{ secrets.DB_USER }}" \
-password="${{ secrets.DB_PASS }}" \
migrate
Spring Boot Integration
# application.properties
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
spring.flyway.clean-disabled=true
spring.flyway.validate-on-migrate=true
spring.flyway.out-of-order=false
Troubleshooting
| Issue | Solution |
|---|---|
| Migration checksum mismatch | Never modify applied migrations; use flyway repair to fix history table |
| Out of order error | Set outOfOrder=true or apply migrations sequentially |
| Baseline needed for existing DB | Run flyway baseline before first migrate or set baselineOnMigrate=true |
| Driver not found | Add JDBC driver to flyway/drivers/ directory |
| Schema history table missing | Run flyway baseline or let migrate create it automatically |
| Clean accidentally run | Restore from backup; set cleanDisabled=true in production configs |
| Migration failed mid-way | Fix the SQL, run flyway repair to remove failed entry, then migrate again |
| Placeholder not resolved | Check placeholder syntax ${name} and ensure it is defined in config |
| Multiple schemas | Set flyway.schemas to comma-separated list; first schema is the default |