Automate Database Migrations

Version-control SQL schema changes and automate them with GitHub Actions.

Introduction

30 Second Summary

Every app update risks breaking the database. One missing column or one wrong table change can take down a production system. Most teams track code changes religiously while letting database changes happen by hand.

In this project, you will build an automated database migration pipeline that versions your schema changes in SQL files, tests them locally with Docker, and deploys them automatically through GitHub Actions on every push.

What You'll Build

You push a SQL file to your repository, and within seconds a fresh PostgreSQL database spins up, runs every migration in order, and reports whether they all passed or failed.

By the end of this project, you'll have:

  • A version-controlled migration system where you write a SQL file, push it to GitHub, and your database schema updates itself automatically with a full audit trail of every change.
  • A local testing environment powered by Docker Compose where a single command spins up PostgreSQL and Flyway together, proving your migrations work before you push.
  • A proven rollback strategy using the roll-forward pattern. When a migration fails, you know exactly how to repair the broken state and fix it with the next versioned migration.
  • Secret Mission: Add a pull request validation workflow that tests migrations against a fresh database before they can be merged to main.

Are there any prerequisites?

No prior experience with Docker, Flyway, or CI/CD is required. Step 1 walks you through installing and configuring every tool from scratch.

Before We Start

Before we start building, let's get clear on what you're about to create and why automating database migrations matters for real-world software teams.

Write Your First Migration and Run It Locally

Your development environment is ready with Docker, Git, and VS Code. Now it's time to put them to work.

Before automating anything, you need to see Flyway work with your own eyes. In this step, you'll write a SQL migration file, run it against a real PostgreSQL database, and watch Flyway track the change.

In this step, get ready to:
  • Create the project folder structure and a Docker Compose file to define your services.
  • Write your first versioned SQL migration.
  • Run the migration locally and confirm Flyway applied it.
Create the project structure and Docker Compose file

Flyway needs a folder to find your SQL migration files. Docker Compose needs a configuration file to know which services to start.

  • Create the migrations folder by running this command in your terminal:
mkdir migrations
  • Confirm the folder was created by running:
ls

You should see migrations and README.md listed in the output.

Don't see the migrations folder?

Make sure your terminal is inside the db-migrations directory. Run pwd (macOS/Linux) or cd (Windows) to check your current location.

Help me figure out why my mkdir command didn't work.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the db-migrations folder on your Desktop, then click Open.

You should see the migrations folder and README.md in the VS Code Explorer sidebar on the left.

  • Create a new file in the project root by right-clicking in the Explorer sidebar and selecting New File.
  • Name it docker-compose.yml.
  • Paste the following code into docker-compose.yml:
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: shopdb
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

  flyway:
    image: redgate/flyway:12.11.0
    depends_on:
      db:
        condition: service_healthy
    environment:
      FLYWAY_URL: jdbc:postgresql://db:5432/shopdb
      FLYWAY_USER: postgres
      FLYWAY_PASSWORD: postgres
    volumes:
      - ./migrations:/flyway/sql
    command: migrate

What does this file do?

This Docker Compose file defines two services that work together:

  • db runs a PostgreSQL 16 database with a health check. The health check uses pg_isready to confirm Postgres is accepting connections before anything else starts.
  • flyway waits for the database to be healthy (condition: service_healthy), then runs all SQL files from the ./migrations folder against the database.

The volumes line maps your local migrations folder into the Flyway container at /flyway/sql. This is where Flyway looks for migration files by default.

  • Save docker-compose.yml with Cmd+S (macOS) or Ctrl+S (Windows).

Getting YAML indentation errors later?

YAML is sensitive to spaces. Make sure you use spaces (not tabs) for indentation. Each nested level should be indented by exactly 2 spaces. If you pasted the code and see red squiggly lines, check that no tabs snuck in.

Help me fix my docker-compose.yml formatting.

✔️ Awesome, I've got everything!

Great. Make sure you've saved docker-compose.yml before moving on.

ⓧ I'd like to double check the full code

Here is the complete docker-compose.yml file. Make sure yours matches exactly:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: shopdb
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

  flyway:
    image: redgate/flyway:12.11.0
    depends_on:
      db:
        condition: service_healthy
    environment:
      FLYWAY_URL: jdbc:postgresql://db:5432/shopdb
      FLYWAY_USER: postgres
      FLYWAY_PASSWORD: postgres
    volumes:
      - ./migrations:/flyway/sql
    command: migrate
Write your V1 migration

Flyway uses a strict naming convention to determine the order migrations run. Every versioned migration starts with a V prefix, then a version number, then a double underscore __, then a description, and ends with .sql.

  • In the VS Code Explorer sidebar, right-click the migrations folder and select New File.
  • Name it exactly V1__Create_products_table.sql.
  • Paste the following SQL into the file:
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

What does this migration do?

This SQL creates a products table with five columns: a unique ID that auto-increments, a product name, an optional description, a price with two decimal places, and a timestamp recording when the row was created.

The filename V1__Create_products_table.sql tells Flyway this is version 1. The double underscore separates the version number from the description. Flyway uses this naming to determine run order and to track which migrations have already been applied.

  • Save V1__Create_products_table.sql with Cmd+S (macOS) or Ctrl+S (Windows).
  • In your terminal, confirm the file exists by running:
ls migrations

You should see V1__Create_products_table.sql in the output.

Don't see the file?

Double check that you created the file inside the migrations folder, not in the project root. The filename must start with a capital V and use two underscores (not one) between the version number and description.

Help me check my migration file naming.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the SQL file before moving on.

ⓧ I'd like to double check the full code

Here is the complete V1__Create_products_table.sql file:

CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);
Run docker compose up and observe

Both files are in place. Time to start the services and watch Flyway apply your migration to the database.

Before you run this, what do you think will happen? Will Flyway find your SQL file and apply it, or will something go wrong?

  • Start both services by running this command in your terminal:
docker compose up

What should I see?

Docker pulls the PostgreSQL and Flyway images (this may take a minute on first run). PostgreSQL starts and reports it's ready to accept connections. Then Flyway connects and applies your migration.

Look for a line in the output that says Successfully applied 1 migration to schema followed by the schema name. This confirms Flyway found your V1 file and ran it against the database.

Seeing an error instead?

  • If you see a message about the Docker daemon not running, open Docker Desktop and wait for it to fully start before trying again.
  • If you see a port conflict on 5432, another process is using that port. Stop any existing PostgreSQL instance or change the port mapping in docker-compose.yml.
  • If Flyway reports no migrations found, make sure your SQL file is inside the migrations folder and the filename starts with V1__ (capital V, two underscores).

Help me troubleshoot my docker compose up error.

Once Flyway finishes, you'll see the containers stay running. The terminal is attached to the container logs.

  • Press Ctrl+C to stop the running containers.
  • Clean up the stopped containers by running:
docker compose down

This stops and removes the containers. Your migration files stay safe on disk.

You just ran your first database migration. Flyway read your SQL file, applied it to PostgreSQL, and recorded it in its history. Next up, you'll add more migrations and see what happens when one of them breaks.

Evolve the Schema and Break It

A real database does not stay static. As your application grows, you add new tables and create relationships between them. Flyway handles this by processing migration files in version order, building up your schema one change at a time.

But what happens when one of those changes has a bug? In this step, you'll add two new migrations and deliberately break one to see exactly how Flyway handles failure.

In this step, get ready to:
  • Add two new migration files to evolve the product catalog schema.
  • Observe how Flyway reports and records a failed migration.
Add V2 and V3 migrations

Your product catalog needs a way to organise products into categories. You'll create two migrations: one that adds a categories table, and one that links products to those categories.

  • In VS Code, create a new file called V2__Add_categories_table.sql inside the migrations folder.
  • Paste the following SQL into the file:
CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL UNIQUE,
    description TEXT
);

What does this migration do?

This creates a categories table with an auto-incrementing id, a unique category name, and an optional description. Products will reference this table once V3 links them together.

  • Save V2__Add_categories_table.sql.
  • Confirm it exists by running:
ls migrations/

You should see V1__Create_products_table.sql and V2__Add_categories_table.sql listed.

Now create the migration that links products to categories. This one will contain a deliberate mistake so you can see what a real-world failure looks like.

  • Create a new file called V3__Link_products_to_categories.sql inside the migrations folder.
  • Paste the following SQL into the file:
-- Deliberately broken version (for learning purposes)
ALTER TABLE products ADD COLUMN category_id INTEGER;

ALTER TABLE products
    ADD CONSTRAINT fk_products_category
    FOREIGN KEY (category_id) REFERENCES categories(category_id);

Where is the bug?

The foreign key references categories(category_id), but look back at V2. The primary key column in the categories table is called id, not category_id. PostgreSQL will reject this constraint because the referenced column does not exist.

This is a common real-world mistake. Column names drift between teams, or you copy-paste from a different schema and forget to update the reference.

  • Save V3__Link_products_to_categories.sql.
  • Confirm all three migration files exist by running:
ls migrations/

You should see V1__Create_products_table.sql, V2__Add_categories_table.sql, and V3__Link_products_to_categories.sql listed.

✔️ Awesome, I've got everything!

Great. Make sure all three files are saved before moving on.

ⓧ I'd like to double check the full code

Here is V2__Add_categories_table.sql:

CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL UNIQUE,
    description TEXT
);

Here is V3__Link_products_to_categories.sql:

-- Deliberately broken version (for learning purposes)
ALTER TABLE products ADD COLUMN category_id INTEGER;

ALTER TABLE products
    ADD CONSTRAINT fk_products_category
    FOREIGN KEY (category_id) REFERENCES categories(category_id);
Run migrations and observe the failure

You have three migration files ready. Time to run them against a fresh database and see what happens when V3 hits that column mismatch.

  • Clear any existing database volume from the previous step by running:
docker compose down -v

What does the -v flag do?

The -v flag removes Docker volumes along with the containers. This deletes the PostgreSQL data directory, giving you a completely fresh database with no tables and no flyway_schema_history table.

Before you run the next command, think about what you expect to happen. Flyway will process V1, then V2, then V3 in order. Which migrations do you think will succeed, and which will fail?

  • Start the containers and run all migrations by running:
docker compose up

Watch the terminal output carefully. Flyway applies V1 and V2 successfully, then hits V3 and stops with a SQL error mentioning the missing column.

What just happened?

Flyway processes migrations sequentially. V1 created the products table. V2 created the categories table. Both succeeded and were recorded in the flyway_schema_history table with a Success state.

V3 failed because PostgreSQL could not find a column called category_id in the categories table. Flyway recorded this failure in the flyway_schema_history table with a Failed state.

  • Press Ctrl+C to stop the running containers.
  • Try running Flyway again to see what happens:
docker compose run --rm flyway migrate

Flyway refuses to proceed. It detects the failed migration record in the schema history and blocks all future migrations until the problem is resolved.

Why does Flyway block further migrations?

The flyway_schema_history table is Flyway's single source of truth. A Failed entry means the database is in an unknown state. Flyway refuses to apply any new migrations because it cannot guarantee what already exists in the schema.

This is a safety mechanism. In production, running new migrations on top of a partially-applied failure could corrupt your data.

Not seeing the error on V3?

Make sure V3__Link_products_to_categories.sql references categories(category_id) and not categories(id). The file must have the incorrect column name for the error to trigger.

Also confirm you ran docker compose down -v before starting. Without the -v flag, the old database volume persists and migrations may behave unexpectedly.

Help me debug my Flyway migration failure.

Your database is stuck. Flyway recorded the failure and now refuses to move forward. In the next step, you'll learn how to recover from this state and implement a rollback strategy.

Recover with Repair and Roll Forward

In the previous step, you saw what a failed migration looks like. Flyway is now stuck. It refuses to run any new migrations because the flyway_schema_history table has a failed record in it.

This step teaches you two critical recovery skills. First, you will use the repair command to clear the broken record and unblock Flyway. Second, you will learn the roll-forward rollback pattern, where you undo a previous change by writing a new migration that reverses it.

In this step, get ready to:
  • Repair the broken schema history and fix the V3 migration.
  • Write a V4 migration that adds inventory columns, and a paired V5 migration that removes them.
  • Run the full migration set and view the complete history with flyway info.
Repair the failed state and fix V3

The repair command removes failed migration entries from the flyway_schema_history table. Once the bad record is gone, Flyway is unblocked and will attempt migrations again.

  • Clear the failed migration record by running this command:
docker compose run --rm flyway repair

What does this command do?

The repair command scans the flyway_schema_history table and removes entries for failed migrations. The --rm flag removes the temporary Flyway container after the command finishes.

After repair completes, the failed V3 record is gone. Flyway no longer sees a blocking failure.

  • You should see JSON output confirming the repair succeeded.

Repair command failed?

Make sure Docker Desktop is still running. The database container from the previous step must be up for the repair to connect. If it is not running, start it with docker compose up -d db first, then retry the repair command.

Help me debug this

The history is cleared, but the V3 file still contains the broken SQL. You need to fix it before running migrations again.

  • In VS Code, open migrations/V3__Link_products_to_categories.sql.
  • Find this line:
FOREIGN KEY (category_id) REFERENCES categories(category_id);
  • Replace categories(category_id) with categories(id) so the line reads:
FOREIGN KEY (category_id) REFERENCES categories(id);
  • Also delete the comment line at the top of the file (-- Deliberately broken version (for learning purposes)).
  • Save the file.

What did you fix?

The categories table defines its primary key column as id. The broken version referenced category_id, which does not exist. Foreign keys must point to the exact column name in the referenced table.

Now run a fresh migration cycle to confirm the fix works.

  • Remove the old database volume and restart everything by running these commands:
docker compose down -v
docker compose up
  • Watch the terminal output. Flyway should report that all three migrations (V1, V2, V3) were successfully applied.
  • Press Ctrl+C to stop the containers once you see the success output.

V3 still failing?

Double check that V3__Link_products_to_categories.sql says REFERENCES categories(id) and not REFERENCES categories(category_id). The column name must match the primary key in V2__Add_categories_table.sql exactly.

Help me find the mismatch

✔️ Awesome, I've got everything!

Great. Confirm you have saved V3__Link_products_to_categories.sql with the corrected foreign key reference.

ⓧ I'd like to double check the full code

Your V3__Link_products_to_categories.sql should look exactly like this:

ALTER TABLE products ADD COLUMN category_id INTEGER;

ALTER TABLE products
    ADD CONSTRAINT fk_products_category
    FOREIGN KEY (category_id) REFERENCES categories(id);
Add V4 and demonstrate the rollback pattern with V5

In production, you cannot simply delete a migration that has already been applied. The roll-forward pattern handles this by writing a new versioned migration that undoes the previous change. The undo becomes part of the version history, fully auditable.

Why roll forward instead of using flyway undo?

Flyway has an undo command, but it requires the paid Teams edition. The roll-forward pattern (writing a new V-prefixed migration that reverses changes) is how most teams using Flyway Community handle rollbacks.

Flyway's official documentation recommends this approach: create a new versioned migration script and roll forward any necessary changes.

  • Create a new file migrations/V4__Add_inventory_tracking.sql and add the following code:
ALTER TABLE products ADD COLUMN stock_quantity INTEGER DEFAULT 0;
ALTER TABLE products ADD COLUMN last_restocked TIMESTAMP;

What does V4 do?

This migration adds two columns to the products table: stock_quantity (an integer defaulting to 0) and last_restocked (a timestamp). These would track inventory for a product catalog.

  • Create another new file migrations/V5__Undo_inventory_tracking.sql and add the following code:
ALTER TABLE products DROP COLUMN last_restocked;
ALTER TABLE products DROP COLUMN stock_quantity;

What does the V4 and V5 pair demonstrate?

V5 removes the columns that V4 added. If V4 turns out to be a mistake, you deploy V5 as the next migration in the sequence. Both are recorded in the schema history, creating a clear audit trail.

This is safer than editing or deleting old migrations, which would break Flyway's checksum validation on any database that already applied them.

  • Save both files.
  • Confirm the files exist by running:
ls migrations/
  • You should see five files: V1__Create_products_table.sql, V2__Add_categories_table.sql, V3__Link_products_to_categories.sql, V4__Add_inventory_tracking.sql, and V5__Undo_inventory_tracking.sql.

✔️ Awesome, I've got everything!

Great. Confirm you have saved both new migration files.

ⓧ I'd like to double check the full code

Your V4__Add_inventory_tracking.sql:

ALTER TABLE products ADD COLUMN stock_quantity INTEGER DEFAULT 0;
ALTER TABLE products ADD COLUMN last_restocked TIMESTAMP;

Your V5__Undo_inventory_tracking.sql:

ALTER TABLE products DROP COLUMN last_restocked;
ALTER TABLE products DROP COLUMN stock_quantity;
Run the full migration set

All five migration files are ready. Time to apply them from scratch against a fresh database and prove the entire sequence works end to end.

  • Stop containers and remove the database volume, then restart by running:
docker compose down -v
docker compose up

Before you look at the output, what do you expect to see? Will all five migrations succeed, or will something block them?

  • Watch the terminal. Flyway applies V1 through V5 in order. All five should succeed with no errors.
  • Press Ctrl+C to stop the containers.

What just happened to the database?

V1 created the products table. V2 created the categories table. V3 linked products to categories via a foreign key. V4 added inventory columns. V5 immediately removed them.

The final schema has the products table with a category_id column but no inventory columns, because V5 undid V4.

Now inspect the migration history to see every version recorded with its timestamp and description.

  • Start the database container in the background and view the full migration history by running:
docker compose up -d db
docker compose run --rm flyway info
  • You should see a table listing all five migrations with their version numbers, descriptions, and a Success state for each.

What does flyway info show?

The info command prints the details and status of all migrations. Each row shows the version, description, type, installation date, and state. This is your audit log of every schema change that has ever been applied to this database.

Seeing a connection error with flyway info?

The docker compose up -d db command starts only the database container in detached mode. Wait a few seconds for PostgreSQL to pass its health check before running the flyway info command. If it still fails, try again after 5 seconds.

Help me fix this connection issue

You now know how to recover from a failed migration and how to safely reverse a schema change. Next up, you will automate this entire workflow so it runs on every push to GitHub.

Automate with GitHub Actions

In Step 4, you repaired a broken migration and proved the roll-forward pattern works. Your local migration system handles schema changes, failures, and recovery.

Running migrations manually is fine during development. For production, you want every push to main to automatically validate and apply migrations against a database, catching errors before they reach production. That is exactly what a CI/CD pipeline does.

In this step, get ready to:
  • Create a GitHub Actions workflow file that triggers on pushes to main.
  • Commit and push all project files to your GitHub repository.
  • Verify the workflow runs and applies all 5 migrations automatically.
Create the workflow file

GitHub Actions workflows live in a special directory inside your repository. When you push code that includes a workflow file, GitHub automatically detects and runs it based on the trigger rules you define.

  • Create the required directory structure by running this command:
mkdir -p .github/workflows
  • In VS Code, create a new file at .github/workflows/migrate.yml.
  • Paste the following workflow configuration into migrate.yml. This defines when the workflow runs, spins up a PostgreSQL service container, and checks out your code:
name: Database Migration

on:
  push:
    branches: [main]
    paths:
      - 'migrations/**'

jobs:
  migrate:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: shopdb
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

What does this configuration do?

  • on: push: branches: [main] tells GitHub to run this workflow only when code is pushed to the main branch.
  • paths: - 'migrations/**' narrows the trigger further. The workflow only fires when files inside the migrations/ folder change.
  • services: postgres: spins up a fresh PostgreSQL 16 container for the workflow to use. The health check options ensure Postgres is ready before any steps run.
  • actions/checkout@v6 clones your repository code onto the runner so Flyway can access the migration files.
  • Directly below the checkout step in the same file, paste the three Flyway commands that do the actual migration work:
      - name: Run Flyway Info (before)
        run: |
          docker run --rm --network host \
            -v ${{ github.workspace }}/migrations:/flyway/sql \
            redgate/flyway:12.11.0 \
            -url=jdbc:postgresql://localhost:5432/shopdb \
            -user=postgres \
            -password=postgres \
            info

      - name: Run Flyway Migrate
        run: |
          docker run --rm --network host \
            -v ${{ github.workspace }}/migrations:/flyway/sql \
            redgate/flyway:12.11.0 \
            -url=jdbc:postgresql://localhost:5432/shopdb \
            -user=postgres \
            -password=postgres \
            migrate

      - name: Verify Migration Status
        run: |
          docker run --rm --network host \
            -v ${{ github.workspace }}/migrations:/flyway/sql \
            redgate/flyway:12.11.0 \
            -url=jdbc:postgresql://localhost:5432/shopdb \
            -user=postgres \
            -password=postgres \
            info

What do these steps do?

  • --network host lets the Flyway Docker container connect to localhost:5432 where the PostgreSQL service container is listening. Without it, the container cannot reach the database.
  • The workflow runs info before migrating (to show pending migrations), then migrate (to apply them), then info again (to confirm everything applied).
  • -v ${{ github.workspace }}/migrations:/flyway/sql mounts your migration files into the Flyway container at the path it expects.
  • Save migrate.yml.
  • Confirm the file exists by running:
ls .github/workflows/

You should see migrate.yml in the output.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

Scroll up and confirm your migrate.yml contains both chunks in order:

  • The trigger, services, and checkout block (starting with name: Database Migration through uses: actions/checkout@v6).
  • The three Flyway command steps (starting with - name: Run Flyway Info (before) through the final info command).

Make sure the indentation is consistent. The steps: key should be at the same indentation level as services: (4 spaces in from migrate:).

File not showing up?

  • Make sure you ran mkdir -p .github/workflows from inside your db-migrations folder. If you were in a different directory, the .github folder was created in the wrong place.
  • Check the file name is exactly migrate.yml (not migrate.yaml or Migrate.yml). Both .yml and .yaml work for GitHub Actions, but use .yml to match this guide.

Still stuck? Help me find my migrate.yml file

Commit and push everything

Your repository has all the pieces: the Docker Compose file, five migration scripts, and the workflow file. Time to push them to GitHub so the workflow can run.

  • Stage all project files by running:
git add .
  • Commit with a descriptive message by running:
git commit -m "Add Flyway migrations and CI workflow"
  • Push to the main branch by running:
git push origin main

Your terminal shows the push progress and confirms the objects were written to your remote repository.

Push rejected or authentication error?

  • If you see a permission error, GitHub may need authentication. Run gh auth login (if you have the GitHub CLI) or set up a personal access token in your repository settings.
  • If the push is rejected because the remote has changes you do not have locally (from the README you created on GitHub), run git pull origin main --rebase first, then push again.

Need more help? Help me fix my git push error

Watch the workflow run

The push you just made changed files inside migrations/, which matches the path filter in your workflow. GitHub should have already started running it.

Before you check, what do you think the workflow will show? Will all 5 migrations apply cleanly to a fresh database, or will something fail?

  • Open your repository on GitHub in a browser.
  • Click the Actions tab near the top of the page.
  • In the left sidebar, click Database Migration.
  • Click the workflow run named Add Flyway migrations and CI workflow (your commit message).
  • Under Jobs, click the migrate job to expand its steps.
  • Click the Run Flyway Migrate step to view its log output.

You should see a green checkmark next to every step. The Run Flyway Migrate log shows Flyway applying all 5 migrations in order. The Verify Migration Status step shows the full history table with each version marked as Success.

Why a fresh database every run?

The PostgreSQL service container starts empty each time the workflow runs. This means every run validates your migrations from scratch, proving they work from a clean slate.

In a production setup, you would point Flyway at a persistent database and it would only apply new migrations that have not yet been recorded in the flyway_schema_history table.

Workflow not appearing in the Actions tab?

  • Make sure you pushed to the main branch (not master or another branch). Check with git branch in your terminal.
  • Verify the file is at exactly .github/workflows/migrate.yml in your repository. If the path is wrong, GitHub cannot detect the workflow.
  • The workflow may take 10-30 seconds to appear. Refresh the Actions tab if you do not see it immediately.

Still stuck? Help me debug my GitHub Actions workflow

Your migrations now deploy themselves. Every time you push a new SQL file to the migrations/ folder on main, GitHub Actions spins up a fresh database, validates every migration from V1 onward, and reports the result. Next up, you will take this one step further with a Secret Mission.

Secret mission

Add a PR Validation Workflow

Your main workflow applies migrations after code reaches main. But what if a broken migration slips through? Build a second workflow that validates every migration against a fresh database on pull requests, catching errors before they ever touch your production branch.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely on Docker containers and a free GitHub repository, so there are no ongoing costs.

Resources you used:

  • Docker containers (PostgreSQL and Flyway) created by docker compose up.
  • Docker volume storing the PostgreSQL database data.
  • Local project folder db-migrations on your Desktop.
  • GitHub repository db-migrations (free tier, no cost).
  • GitHub Actions minutes used (free tier includes 2,000 minutes/month for public repos).

Keep everything running

No action needed. Choose this if you are still actively building on this project or plan to add more migrations soon.

  • Your repository stays on GitHub. Run docker compose up anytime to test migrations locally.
  • Docker containers are already stopped if you ran docker compose down during the project. They only consume resources while actively running.
  • The Docker images (postgres:16 and redgate/flyway:12.11.0) remain cached on your machine for fast startup next time.

Pause - I'll come back to this later

Shut down running containers to free up system resources. Your files and repository stay intact for when you return.

  • Stop any running containers by running this command from inside the db-migrations folder:
docker compose down
  • This stops and removes the containers, but keeps your local files, Docker images, and GitHub repository intact.
  • When you are ready to resume, run docker compose up again from the project folder.

Delete - I don't want to use this again

Remove all project resources from your machine and GitHub. This is permanent.

  • Stop containers and remove the database volume by running this from inside the db-migrations folder:
docker compose down -v
  • Remove the cached Docker images by running:
docker rmi postgres:16 redgate/flyway:12.11.0
  • Delete the local project folder. Navigate up one level and remove it:

macOS

cd ~/Desktop
rm -rf db-migrations

Windows

cd ~\Desktop
Remove-Item -Recurse -Force db-migrations
  • Delete the GitHub repository:
  • Go to your db-migrations repository on GitHub.
  • Click Settings.
  • Scroll down to the Danger Zone section.
  • Click Delete this repository.
  • Click I want to delete this repository.
  • Click I have read and understand these effects.
  • Type db-migrations to confirm, then click Delete this repository.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully automated database migration pipeline from scratch.

You've learned how to:

  • Build a version-controlled migration system using Flyway naming conventions (V prefix, double underscore separator) and run migrations locally with Docker Compose.
  • Recover from a failed migration by repairing the schema history, and implement the roll-forward rollback pattern to undo changes safely as the next versioned migration.
  • Automate your entire migration pipeline with GitHub Actions, triggered on every push to main that changes files in the migrations/ directory.
  • Secret Mission: Added a pull request validation workflow that tests migrations against a fresh database before they reach main.

Ready to quiz yourself?