Flyway Database Migration Guide: Setup, CI/CD and Best Practices

Devops & Infrastructure, Security, Tips & Tricks, and What Is

Flyway Database Migration Guide: Setup, CI/CD and Best Practices

Application code gets version control, automated tests, and one-click deployments. Database schemas? Often they're still managed with hand-run SQL scripts and whispered tribal knowledge. That gap between code deployment and database deployment is where things break.

Flyway is a database migration tool that brings version control principles to your schema. You write numbered SQL files, Flyway tracks what's been applied, and every environment stays in sync. Combined with DeployHQ's build pipeline integration, you can automate database migrations as a pre-deployment step --- so your schema and code always ship together.

This guide covers what Flyway is, how it works, how to use it with JVM, Python, and Node.js projects, and how to integrate it into your deployment workflow.

What Is Flyway?

Flyway is an open-source database migration tool developed by Redgate. It applies versioned SQL scripts to your database in a deterministic order, tracking which migrations have already run so they are never re-applied. Think of it as Git for your database schema --- every change is an immutable, ordered script that moves the schema from one known state to the next.

Under the hood, Flyway maintains a metadata table (flyway_schema_history) inside your database. Each row records a migration's version number, description, checksum, and execution timestamp. When you run flyway migrate, it compares the scripts on disk against this table and applies only the pending ones. If someone edits an already-applied script, the checksum mismatch halts execution immediately --- preventing silent schema corruption that manual SQL workflows can't catch.

Flyway supports 50+ databases (PostgreSQL, MySQL, Oracle, SQL Server, SQLite, MariaDB, CockroachDB, Snowflake, and many more), works with any programming language via its CLI, and integrates natively with JVM build tools like Maven and Gradle.

The Database Migration Problem

When database changes are managed manually, teams run into predictable problems:

  • Schema drift --- development, staging, and production databases diverge, causing works on my machine failures
  • Error-prone deployments --- running SQL scripts by hand is tedious and one wrong statement can cause downtime
  • No audit trail --- no record of who changed what, when, or why
  • Difficult rollbacks --- reverting a schema change is far harder than reverting a code commit

Migration tools solve this by treating schema changes as versioned, sequential scripts that are applied automatically.

How Flyway Works

Flyway operates on a straightforward principle: versioned migration scripts.

flowchart LR
    A["Write SQL\nmigration"] --> B["flyway validate"]
    B --> C["flyway migrate"]
    C --> D["Verify\nschema state"]
    D --> E["Deploy\napplication"]
    E --> F{"Issues?"}
    F -- No --> G["Done"]
    F -- Yes --> H["Rollback\nmigration"]
    H --> A
  1. You write migration files --- plain SQL files with version numbers: V1__create_users_table.sql, V2__add_email_column.sql
  2. Flyway tracks state --- a flyway_schema_history table in your database records every applied migration, its checksum, and when it ran
  3. Flyway applies the diff --- on each run, it compares your migration files against the history table and applies only the pending ones, in order
  4. Checksums catch tampering --- if an already-applied script is modified, Flyway detects the mismatch and stops, preventing silent corruption
  5. Every environment stays in sync --- the same migrations run in dev, staging, and production, eliminating drift

Flyway for JVM Projects

Flyway originated in the Java ecosystem and integrates deeply with JVM build tools.

Java API

Run migrations programmatically --- useful for applying them on application startup:

import org.flywaydb.core.Flyway;

public class DatabaseMigrator {
    public static void main(String[] args) {
        Flyway flyway = Flyway.configure()
            .dataSource("jdbc:postgresql://localhost:5432/mydb", "user", "password")
            .locations("classpath:db/migration")
            .load();
        flyway.migrate();
        System.out.println("Database migration completed successfully!");
    }
}

Maven Plugin

Add Flyway to your build lifecycle:

<build>
    <plugins>
        <plugin>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-maven-plugin</artifactId>
            <version>11.8.0</version>
            <configuration>
                <url>jdbc:postgresql://localhost:5432/mydb</url>
                <user>user</user>
                <password>password</password>
                <locations>
                    <location>classpath:db/migration</location>
                </locations>
            </configuration>
        </plugin>
    </plugins>
</build>

Run with: mvn flyway:migrate

Gradle Plugin

plugins {
    id "org.flywaydb.flyway" version "11.8.0"
}

flyway {
    url = 'jdbc:postgresql://localhost:5432/mydb'
    user = 'user'
    password = 'password'
    locations = ['classpath:db/migration']
}

Run with: gradle flywayMigrate

Spring Boot Integration

Spring Boot 3.x auto-configures Flyway when you add the flyway-core dependency --- migrations run automatically on startup from src/main/resources/db/migration.

Note: Spring Boot 4.x changed this --- you now need spring-boot-starter-flyway instead of just flyway-core.

Transactional DDL

For databases that support it (PostgreSQL, SQL Server), Flyway wraps each migration in a transaction. If any statement fails, the entire script rolls back --- no partial schema changes.

Flyway for Non-JVM Projects

Flyway's command-line interface works with any language or framework. The CLI download bundles a JRE, so you don't need Java installed separately.

1. Configure your connection

Create a flyway.conf file:

flyway.url=jdbc:mysql://localhost:3306/mydb
flyway.user=root
flyway.password=password
flyway.locations=filesystem:sql/migrations

2. Organise your migration scripts

sql/migrations/
  V1__create_products_table.sql
  V2__add_price_column.sql
  V3__create_orders_table.sql

Version numbers can use dots or underscores as separators (V2.1__ and V2_1__ are both valid).

3. Run migrations

flyway -configFiles=flyway.conf migrate

4. Integrate with CI/CD

Add Flyway as a pre-deployment step in your pipeline. Here's a shell script example:

#!/bin/bash

export FLYWAY_URL="jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}"
export FLYWAY_USER="${DB_USER}"
export FLYWAY_PASSWORD="${DB_PASSWORD}"
export FLYWAY_LOCATIONS="filesystem:./db/migrations"

echo "Running Flyway migrations..."
./flyway migrate

if [ $? -eq 0 ]; then
    echo "Flyway migrations completed successfully!"
else
    echo "Flyway migrations failed!"
    exit 1
fi

In DeployHQ, you can run this as a build pipeline command or SSH pre-deployment hook, with database credentials stored securely as environment variables.

Flyway with Python Projects

Python projects commonly use Alembic or Django migrations, but Flyway is a strong choice when your team manages multiple services across different languages and wants a single migration tool for all of them. The Flyway CLI runs SQL migrations regardless of your application language.

Using the Flyway CLI from Python

The most reliable approach is calling the Flyway CLI directly from your Python build or deployment scripts:

import subprocess
import sys
import os

def run_flyway_migrate():
    """Run Flyway migrations as part of the deployment pipeline."""
    env = os.environ.copy()
    env.update({
        "FLYWAY_URL": f"jdbc:postgresql://{os.getenv('DB_HOST', 'localhost')}:5432/{os.getenv('DB_NAME', 'mydb')}",
        "FLYWAY_USER": os.getenv("DB_USER", "postgres"),
        "FLYWAY_PASSWORD": os.getenv("DB_PASSWORD", ""),
        "FLYWAY_LOCATIONS": "filesystem:./db/migrations",
    })

    result = subprocess.run(
        ["flyway", "migrate"],
        env=env,
        capture_output=True,
        text=True,
    )

    if result.returncode != 0:
        print(f"Migration failed:\n{result.stderr}", file=sys.stderr)
        sys.exit(1)

    print(result.stdout)
    print("Flyway migrations applied successfully.")

if __name__ == "__main__":
    run_flyway_migrate()

Using the flyway-python wrapper

The flyway PyPI package provides a thin wrapper around the Flyway CLI, downloading and managing the binary automatically:

pip install flyway
# flyway-python exposes the CLI as a Python callable
from flyway import Flyway

flyway = Flyway(
    url="jdbc:postgresql://localhost:5432/mydb",
    user="postgres",
    password="secret",
    locations=["filesystem:./db/migrations"],
)

flyway.migrate()

This is convenient for local development, but for production CI/CD pipelines most teams install the Flyway CLI directly --- it avoids adding a Python dependency to your migration path.

Project structure

A typical Python project using Flyway:

my-python-app/
  app/
    __init__.py
    main.py
  db/
    migrations/
      V1__create_users.sql
      V2__add_sessions_table.sql
  flyway.conf
  requirements.txt
  deploy.py           # calls flyway migrate

Flyway with Node.js Projects

Node.js projects typically use tools like Prisma Migrate or Knex migrations, but Flyway works well when you need a language-agnostic migration tool across a polyglot stack --- for example, when your Node.js API shares a database with Java microservices.

Using the Flyway CLI from Node.js

Call the Flyway CLI as a child process in your deployment scripts:

// scripts/migrate.js
const { execSync } = require("child_process");

const env = {
  ...process.env,
  FLYWAY_URL: `jdbc:postgresql://${process.env.DB_HOST || "localhost"}:5432/${process.env.DB_NAME || "mydb"}`,
  FLYWAY_USER: process.env.DB_USER || "postgres",
  FLYWAY_PASSWORD: process.env.DB_PASSWORD || "",
  FLYWAY_LOCATIONS: "filesystem:./db/migrations",
};

try {
  const output = execSync("flyway migrate", { env, encoding: "utf-8" });
  console.log(output);
  console.log("Flyway migrations applied successfully.");
} catch (error) {
  console.error("Migration failed:", error.stderr || error.message);
  process.exit(1);
}

npm scripts integration

Add migration commands to your package.json:

{
  "scripts": {
    "db:migrate": "flyway -configFiles=flyway.conf migrate",
    "db:validate": "flyway -configFiles=flyway.conf validate",
    "db:info": "flyway -configFiles=flyway.conf info",
    "predeploy": "npm run db:migrate"
  }
}

This lets you run npm run db:migrate locally and hook it into your DeployHQ build pipeline as a build step.

Project structure

my-node-app/
  src/
    index.ts
    routes/
  db/
    migrations/
      V1__create_accounts.sql
      V2__add_api_keys_table.sql
  flyway.conf
  package.json
  scripts/
    migrate.js

Rollback Strategies

Rolling back database migrations is harder than rolling back application code --- you can't simply revert a SQL file that has already altered your schema. Flyway provides several approaches depending on your edition and requirements.

Undo migrations (Teams/Enterprise edition)

Flyway's Teams and Enterprise editions include the flyway undo command. For each versioned migration, you write a corresponding undo script:

db/migrations/
  V1__create_users.sql
  U1__drop_users.sql
  V2__add_email_column.sql
  U2__remove_email_column.sql

Running flyway undo reverses the most recently applied migration. You can run it multiple times to step back through several versions.

Compensation migrations (Community edition)

The free Community edition does not include undo. Instead, the standard pattern is to write a new forward migration that reverses the effect of the problematic one:

-- V3__rollback_add_email_column.sql
-- Compensation migration: reverses V2__add_email_column.sql

ALTER TABLE users DROP COLUMN IF EXISTS email;

This keeps your migration history moving forward --- every change is recorded and auditable, even rollbacks.

Best practices for rollback-safe migrations

  1. Make migrations additive --- prefer ADD COLUMN over DROP COLUMN. Additive changes are inherently backward-compatible and don't require immediate rollback scripts
  2. Use phased removals --- to drop a column, first deploy code that stops reading it, then add a migration to remove it in a later release
  3. Test undo scripts --- if you use the Teams/Enterprise undo command, test undo scripts in CI just as you test forward migrations
  4. Always back up before migrating production --- no rollback strategy replaces a known-good database backup
  5. Consider zero-downtime database migrations for high-availability systems where rollback must be instant

Key Features

  • Plain SQL migrations --- write standard SQL, no proprietary syntax required
  • Checksum validation --- prevents silent modifications to already-applied scripts
  • Baseline support --- bring an existing database under Flyway control without replaying history
  • Validation --- detect inconsistencies between your scripts and database state with flyway validate
  • Clean command --- wipe schemas for dev/test environments. Warning: this drops ALL objects in configured schemas. Never use in production. Set flyway.cleanDisabled=true in production configs
  • Undo migrations --- available in the Enterprise edition. The free Community edition requires compensation migrations (see Rollback Strategies above)
  • 50+ databases supported --- MySQL, PostgreSQL, Oracle, SQL Server, SQLite, MariaDB, CockroachDB, Snowflake, and many more

Best Practices

  1. One change per migration --- each script should be a single, atomic schema change. Don't bundle unrelated changes
  2. Descriptive names --- V3__add_index_on_users_email.sql is better than V3__update.sql
  3. Test before production --- run migrations in staging first. Tools like Testcontainers can spin up isolated databases for automated testing
  4. Design for backward compatibility --- avoid destructive changes (dropping columns) if the current app version depends on them. Use a phased approach: deprecate first, remove in a later migration
  5. Commit migrations to Git --- store them alongside your application code so schema history tracks with your codebase. Deploy from GitHub or GitLab and DeployHQ picks them up automatically
  6. Automate with CI/CD --- run migrations as a pre-deployment step, not manually. DeployHQ's build pipeline integration handles this natively
  7. Use minimal database privileges --- create a dedicated migration user with only the permissions Flyway needs
  8. Always back up before migrating production --- Flyway is reliable, but backups are non-negotiable

Troubleshooting Common Flyway Errors

Checksum mismatch

ERROR: Migration checksum mismatch for migration version 3
-> Applied to database : 1234567890
-> Resolved locally    : 9876543210

Cause: Someone edited a migration file after it was already applied to the database. Flyway refuses to continue because the script on disk no longer matches what was executed.

Fix: If the edit was intentional (e.g., fixing a comment), run flyway repair to update the stored checksum. If the edit changed actual SQL, write a new migration instead --- never modify applied scripts.

Out-of-order migrations

ERROR: Detected resolved migration not applied to database: 2.1

Cause: A migration with a lower version number was added after a higher version was already applied. This happens when two developers create migrations in parallel.

Fix: Enable out-of-order execution in your config:

flyway.outOfOrder=true

This tells Flyway to apply any pending migration regardless of version ordering. Use this cautiously --- it works well for additive changes but can cause issues if migrations have interdependencies.

Failed migration leaves database in dirty state

ERROR: Found non-empty schema(s) "public" but no schema history table.

Or after a partial failure:

ERROR: Detected failed migration to version 5 (add_orders_table)

Cause: A migration failed partway through. On databases without transactional DDL (like MySQL for DDL statements), the partial changes remain applied but Flyway marks the migration as failed.

Fix:

  1. Manually inspect and fix the database state
  2. Run flyway repair to remove the failed migration record from the history table
  3. Fix the migration script and re-run flyway migrate

Connection refused or authentication failures

ERROR: Unable to obtain connection from database

Cause: Wrong JDBC URL, incorrect credentials, or the database server is unreachable.

Fix: Verify your connection details. Test with a direct database client first:

# PostgreSQL
psql -h localhost -p 5432 -U user -d mydb

# MySQL
mysql -h localhost -P 3306 -u root -p mydb

Then confirm the JDBC URL format matches your database. Common mistakes include using mysql:// instead of jdbc:mysql:// or forgetting the port number.

How Flyway Works with DeployHQ

DeployHQ supports Flyway through its build pipeline and hook system:

  • Build pipelines --- run flyway migrate (or Maven/Gradle commands) as a build step before files are deployed
  • Pre-deployment SSH hooks --- execute migrations directly on your server before the new code goes live
  • Environment variables --- store DB_HOST, DB_USER, DB_PASSWORD securely without hardcoding credentials
  • Deployment history --- full visibility into every deployment, including migration outcomes
  • One-click rollback --- revert your application deployment if a migration causes issues
  • Deploy from GitHub or GitLab --- your migration scripts deploy alongside your code from any provider

For a broader look at how other frameworks handle database migrations during deployments, see our guide on .NET database migrations with DbUp.

Flyway vs Alternatives

Flyway isn't the only migration tool. Here's how it compares:

Tool Ecosystem Key Difference
Liquibase Java / cross-platform Supports XML/YAML/JSON changelogs in addition to SQL. More enterprise-oriented
Alembic Python (SQLAlchemy) Auto-generates migrations from model changes. Python-native
Prisma Migrate Node.js / TypeScript Schema-first approach with auto-generated SQL
Atlas Go / cross-platform Declarative schema-as-code with HCL syntax
Django Migrations Python (Django) Built into the framework, auto-detects model changes

Flyway's strength is its simplicity: plain SQL files, no proprietary format, and it works with any language via the CLI.

Licensing

Flyway offers two editions:

  • Community (free, open source) --- core migration features, checksum validation, CLI + Maven + Gradle support
  • Enterprise (paid) --- adds undo migrations, dry runs, Oracle SQL*Plus support, and priority support from Redgate

The free Community edition covers the majority of use cases.

FAQ

Q: Does Flyway work with MySQL? A: Yes. Flyway supports 50+ databases including MySQL, PostgreSQL, Oracle, SQL Server, SQLite, MariaDB, and CockroachDB. See the full list.

Q: Do I need Java installed to use Flyway? A: Not if you use the CLI --- it bundles a JRE. For JVM projects using Maven/Gradle plugins, you'll already have Java in your build environment.

Q: Can I use Flyway with a Node.js or Python project? A: Yes. Use the Flyway CLI to run migrations from any language. See the dedicated Python and Node.js sections above for integration examples.

Q: Is Flyway free? A: The Community edition is free and open source. The Enterprise edition adds advanced features like undo migrations. See Redgate's pricing for details.

Q: What happens if I accidentally modify an already-applied migration? A: Flyway detects the checksum mismatch and halts, preventing further migrations. You can use flyway repair to update the stored checksum if the change was intentional. See the Troubleshooting section above for details.

Q: How do I roll back a Flyway migration? A: The Enterprise edition provides the flyway undo command. With the free Community edition, write a compensation migration --- a new forward migration that reverses the previous change. See Rollback Strategies above for full details.


Database migrations don't have to be a manual, error-prone process. Flyway gives you version-controlled, repeatable schema changes --- and when paired with DeployHQ's automated deployments, those migrations run automatically as part of every deployment.

Start your free DeployHQ trial and automate your database migrations alongside your code deployments.


Questions? Reach out at support@deployhq.com or @deployhq.