Flyway-+-MSSQL Developer Guide
Database migrations with Flyway against SQL Server — local dev setup via Docker Compose, migration authoring, and GitHub Actions CI/CD.
Table of Contents
- How Flyway Works
- Project Structure
- Local Development
- Writing Migrations
- Running Migrations Locally
- Flyway Configuration Reference
- GitHub Actions CI/CD
- Common Workflows
- Troubleshooting
1. How Flyway Works
Flyway tracks which SQL scripts have been applied to a database using a metadata table (flyway_schema_history) it creates automatically on first run. Each migration is a versioned SQL file. Flyway compares the files in your migrations directory against what’s recorded in the history table and runs anything that hasn’t been applied yet — in version order.
Key rules:
- Once a migration is applied and committed, never edit it — Flyway checksums every file and will error if a previously-applied migration changes
- Version numbers must be unique and increase monotonically
- Use
R__prefix for repeatable migrations (views, stored procedures, functions) that re-run whenever their content changes
2. Project Structure
For the current database structure and table relationships, see Database ERD.
Migration files live in src/sql/ and follow the naming convention V{major}.{minor}.{patch}__{description}.sql.
3. Local Development
Local dev uses Docker Compose directly — no dev container required. Three services coordinate the startup sequence:
Setup
- Copy
.env.exampleto.envand fill in your values:
- Run the commands listed in Section 5.
SQL Server’s SA password must meet complexity requirements: min 8 chars, mix of uppercase, lowercase, digits, and symbols.
Docker Compose Services
db — SQL Server. The port is mapped from ${SQL_PORT} on the host to 1433 in the container. Data is persisted in the mssql_data volume.
db-init — Runs after the db service is healthy. Creates the database defined by ${SQL_DATABASE} if it doesn’t already exist, using the script below.
flyway — Runs flyway migrate using:
src/sql/mounted to/flyway/sqlflyway.confmounted to/flyway/conf/flyway.conf- Environment variables loaded from
.env
Flyway depends on db-init completing successfully before it runs.
docker-compose.yml
src/scripts/db-init.sh
Run by the db-init container. Waits for SQL Server to accept connections, then creates the target database if it doesn’t already exist.
4. Writing Migrations
Naming Convention
SQL Server Migration Patterns
Create a table
Add a column (with a default for existing rows)
Add a foreign key
Rename a column (SQL Server pattern — no direct RENAME)
Create or alter a stored procedure (use repeatable migration)
Data migration with transaction
Conditional DDL (idempotent pattern when needed)
SQL Safety Rules
These rules apply to all SQL written in migration files.
Never use 'NULL' as a string literal — use the SQL NULL keyword without quotes.
Convert booleans explicitly to integers (1 or 0) for BIT fields.
Never interpolate variables directly into SQL strings in application code that calls the database — always use parameterized queries.
This rule applies to any application code (Node.js, C#, etc.) that executes dynamic SQL, not to static migration files themselves:
What NOT to Do in a Migration
❌ Never edit or delete an already-applied migration file
Flyway checksums every file. If you change a file that has already been applied, every subsequent flyway migrate will fail with a checksum mismatch. The only safe fix is to revert the file or run flyway repair (local dev only).
❌ Never leave transactions open — always COMMIT or ROLLBACK explicitly An unclosed transaction causes Flyway to hang indefinitely waiting for the lock to release.
❌ Never use SELECT * in a migration
Schema changes in a later migration can silently break a migration that relied on SELECT * column ordering.
✅ Keep each migration focused on one logical change
One migration = one concern. This makes rollback strategies clearer and failure diagnosis easier.
✅ Use IF NOT EXISTS / IF EXISTS guards when writing additive DDL
Guards make migrations safe to re-run during development and protect against partial-failure replays.
5. Running Migrations Locally
Docker Compose Commands
Pass any Flyway subcommand (
migrate,info,validate,repair,baseline) to theflywayservice viadocker compose run --rm flyway <command>.
6. Flyway Configuration Reference
flyway.conf (committed to repo)
Environment variable placeholders (${VAR}) are resolved at runtime from the .env file (loaded via env_file: in docker-compose.yml) or from the shell environment in CI/CD.
In CI/CD, add a second location if you download the SQL files as an artifact to a separate path:
.env (local — gitignored)
JDBC URL Reference
7. GitHub Actions CI/CD
Workflow: Database Migration (.github/workflows/database-migration.yml)
Trigger: Manual (workflow_dispatch) with environment selection
Runs Flyway migrations against an Azure SQL database. Authentication uses Azure AD Managed Identity (Authentication=ActiveDirectoryDefault) — no password is stored in GitHub.
The workflow:
- Logs in to Azure via OIDC (Workload Identity Federation — no stored secrets)
- Checks connectivity to the target SQL server
- Checks out the repository
- Uploads
src/sql/as a GitHub Actions artifact (for audit trail) - Installs Flyway via
red-gate/setup-flyway@v1 - Downloads the artifact to
downloaded_sql_artifacts/(matchesflyway.locationsinflyway.conf) - Runs
flyway migratewith the environment-specific connection URL
Required GitHub Environment variables (configured per environment in Settings → Environments):
No
FLYWAY_PASSWORDis needed —Authentication=ActiveDirectoryDefaultuses the runner’s managed identity or the Azure login from the previous step.
Using SQL auth instead of Managed Identity
If your environment uses SQL authentication, replace the -url and -user flags with a secret-backed password:
Store SQL_PASSWORD as an encrypted secret (not a variable) in the GitHub Environment.
8. Common Workflows
Starting fresh on a new machine
Adding a new migration
Fixing a failed migration
When a migration fails partway through, Flyway marks it as FAILED in the history table. Fix the underlying issue, then:
Resetting local dev database
9. Troubleshooting
FlywayException: Found non-empty schema(s) but no schema history table
Flyway found tables in the schema but no flyway_schema_history table — the database was set up outside Flyway.
Validate failed: Migration checksum mismatch
A previously-applied migration file was edited after it ran.
Never repair checksum mismatches on shared or production databases without understanding why the file changed. The correct fix is to revert the file to its original content.
Connection refused or Login failed
Common mistakes:
SQL_SERVERmust match the container name (mssql_server) when running inside Docker Compose- Password must meet SQL Server complexity requirements: min 8 chars, mix of uppercase, lowercase, digits, and symbols
SQL_USERNAMEdefaults tosafor local dev; Azure SQL uses an AD user or managed identity in CI
db-init times out before SQL Server is ready
The db-init.sh script retries for up to 60 seconds (30 × 2s). If SQL Server takes longer to start:
Migration hangs indefinitely
Most commonly caused by an open transaction in a migration file:
Fix the SQL, then run flyway repair before re-running.
CI: flyway command not found
The CI workflow installs Flyway via red-gate/setup-flyway@v1. If the step fails, check that the runner has internet access to reach the Flyway download endpoint.
CI: Connectivity check fails
The migration workflow checks connectivity before running migrations:
If this fails, check the runner’s network/firewall rules and the Azure SQL server firewall — the runner’s outbound IP must be in the SQL server allowlist.
Flyway docs: flywaydb.org/documentation — SQL Server JDBC driver: Microsoft JDBC Driver for SQL Server