DbUp is the right choice when you want version-controlled, code-reviewed SQL scripts that run exactly once per database — and nothing fancier. If you already live in Entity Framework Core and don't write raw SQL, stick with EF Core Migrations. If you want C# DSL migrations with rollback, look at FluentMigrator. If you want database-first SQL scripts with a transparent SchemaVersions ledger and no framework lock-in, DbUp on .NET 9 plus DeployHQ's build pipeline is the setup documented below.
Migration library comparison: DbUp vs EF Core vs FluentMigrator
| DbUp 6 | EF Core 9 Migrations | FluentMigrator 6 | |
|---|---|---|---|
| Script style | Raw SQL files | C# DbContext snapshot |
C# fluent DSL |
| Supported DBs | SQL Server, PostgreSQL, MySQL, SQLite, Oracle, Firebird, Redshift | EF Core providers | SQL Server, Postgres, MySQL, Oracle, SQLite |
| Rollback support | No (forward-only) | Yes (dotnet ef database update <previous>) |
Yes (Down method) |
| Transactions | Per-script or single | Per-migration | Per-migration |
| Requires ORM? | No | Yes (EF Core) | No |
| Script tracking table | SchemaVersions |
__EFMigrationsHistory |
VersionInfo |
| Best fit | Teams that already write SQL | Greenfield EF Core apps | Database-agnostic C# shops |
Short version: DbUp wins when your DBAs want to read every migration as literal SQL before it hits production. EF Core Migrations wins when nobody on the team writes SQL by hand. FluentMigrator sits in the middle — it's a C# API like EF Core, but it doesn't need a DbContext.
The rest of this guide is DbUp-specific.
What is DbUp?
DbUp is a lightweight .NET library that deploys changes to SQL Server, PostgreSQL, MySQL, and other database systems. Unlike Entity Framework Core Migrations or FluentMigrator, DbUp takes a SQL-first approach: you write plain SQL scripts, and DbUp handles execution order, tracks which scripts have run (in a SchemaVersions table), and ensures each script runs exactly once.
This approach has three concrete advantages:
- Full SQL control — you can use provider-specific features (
SQL Server OPTIMIZE FOR UNKNOWN, PostgresCREATE INDEX CONCURRENTLY, MySQLALGORITHM=INPLACE) without fighting an ORM. - Reviewable diffs — migrations are plain
.sqlfiles your DBA can read without running the tool. - No ORM coupling — works with EF Core, Dapper, ADO.NET, or any mix.
As of April 2026, DbUp is at 6.x on .NET 9 (the current LTS until Nov 2026).
The deployment architecture
Here's how the pieces fit together:
- You push to your Git repository (GitHub, GitLab, Bitbucket, etc.)
- DeployHQ detects the change and triggers a deployment
- Your application is built and deployed to your server
- DeployHQ runs a post-deploy command that executes your DbUp migration console app
- DbUp reads the embedded scripts, compares to
SchemaVersions, runs the ones that haven't run - The deployment is marked complete only if DbUp exits with code
0
If DbUp fails, DeployHQ halts the deployment — so application code never ships ahead of its matching schema. This is the whole point of automated deployment.
Setting Up DbUp: SQL Server, PostgreSQL, or MySQL
The DbUp console app structure is the same across all three databases — only the NuGet package and connection string change.
Step 1: Create the migration console application
In your solution, add a new console application targeting .NET 9:
dotnet new console -n YourProject.DatabaseMigration -f net9.0
Step 2: Install the DbUp package for your database
| Database | NuGet package | Example connection string |
|---|---|---|
| SQL Server | dbup-sqlserver |
Server=sql.example.com;Database=AppDb;User Id=app;Password=...;TrustServerCertificate=True; |
| PostgreSQL | dbup-postgresql |
Host=pg.example.com;Database=appdb;Username=app;Password=...;SSL Mode=Require; |
| MySQL | dbup-mysql |
Server=mysql.example.com;Database=appdb;Uid=app;Pwd=...;SslMode=Required; |
Install the one you need:
dotnet add package dbup-sqlserver
# or
dotnet add package dbup-postgresql
# or
dotnet add package dbup-mysql
Step 3: Structure your migration scripts
Create a Scripts folder. DbUp executes scripts in alphabetical order, so use a zero-padded numeric prefix:
0001-CreateUsersTable.sql0002-AddEmailIndexToUsers.sql0003-CreateOrdersTable.sql
Mark each .sql file as Embedded Resource in its properties. This ships the SQL inside your compiled assembly, so deployments don't need loose SQL files on disk.
Step 4: Write the migration application
Your Program.cs is nearly identical for all three databases — the only difference is the DeployChanges.To.XxxDatabase(...) call.
SQL Server:
using System.Reflection;
using DbUp;
var connectionString = args.FirstOrDefault()
?? throw new ArgumentException("Connection string required");
var upgrader = DeployChanges.To
.SqlDatabase(connectionString)
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
.WithTransactionPerScript()
.LogToConsole()
.Build();
var result = upgrader.PerformUpgrade();
if (!result.Successful)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(result.Error);
Console.ResetColor();
return -1;
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Success!");
Console.ResetColor();
return 0;
PostgreSQL — change the builder line:
var upgrader = DeployChanges.To
.PostgresqlDatabase(connectionString)
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
.WithTransactionPerScript()
.LogToConsole()
.Build();
MySQL — same pattern:
var upgrader = DeployChanges.To
.MySqlDatabase(connectionString)
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
.WithTransactionPerScript()
.LogToConsole()
.Build();
WithTransactionPerScript() is the safest default — if a script fails partway through, its changes roll back. Use WithTransaction() for a single wrapping transaction only if you have a specific reason (some DDL statements won't run inside a transaction on SQL Server, for example).
The non-zero exit code is critical for DeployHQ integration — it's the signal that tells DeployHQ to halt the deployment if migrations fail.
Step 5: Test locally
dotnet run --project YourProject.DatabaseMigration "Server=localhost;Database=YourDb;Trusted_Connection=True;TrustServerCertificate=True;"
On the first run, DbUp creates SchemaVersions and executes your scripts. Run it again and DbUp should skip everything — it's already applied.
Configuring DeployHQ
With your DbUp app working locally, wire it into the deployment pipeline.
Step 6: Set up your DeployHQ project
Log in to DeployHQ and create a new project pointing at your Git repository. Connect your deployment target — SFTP, SSH, Amazon S3, or a custom SSH server. For a typical .NET app, you're probably deploying to a Windows or Linux VM via SFTP or directly to IIS.
Step 7: Configure the build pipeline
In Build Pipeline, add a command to compile and publish your solution:
dotnet publish YourProject.sln -c Release -o ./publish
This publishes everything — web app, API, and the migration console — into a single publish folder that gets shipped to the server.
Step 8: Configure deployment paths
In Deployment Configuration, specify that ./publish is the source of truth for files. The migration console app must be deployed somewhere the server can execute it — typically a sibling folder to your main app.
Step 9: Add the post-deploy migration command
In Commands, add a new command with these settings:
- Command Type:
Run on Server - Trigger:
After files are copied - Command:
cd /path/to/your/deployment/YourProject.DatabaseMigration && dotnet YourProject.DatabaseMigration.dll "$CONNECTION_STRING"
Replace /path/to/your/deployment with the actual path on your server.
Step 10: Configure connection strings as environment variables
In Environment Variables, add CONNECTION_STRING with your database connection string as the value. Keep one variable per environment (staging, production) — DeployHQ lets you set per-server overrides, which is exactly how you avoid migrating production against staging data.
Never hardcode connection strings in your code or in DeployHQ command strings.
Step 11: Run the first deployment
Commit a change (or trigger a manual deploy) and watch the log:
- Code pulled from the repository
dotnet publishruns in the build pipeline- Files transferred to the server
- Migration command runs, DbUp logs each script it applies
- Deployment marked successful — or halted if DbUp exited non-zero
Advanced configuration
Per-environment deployments
Create a separate DeployHQ deployment configuration for each environment (development, staging, production), each with its own CONNECTION_STRING environment variable. This is the standard pattern for preventing cross-environment mistakes.
Transaction strategy
WithTransactionPerScript()(default recommendation) — each script is atomic. If script 3 fails, scripts 1 and 2 stay, script 3 rolls back.WithTransaction()— all scripts in one transaction. Fails atomically but can't be used with certain DDL statements (SQL ServerCREATE INDEX WITH ONLINE=ON, for example).WithoutTransaction()— required for operations that explicitly forbid transactions (PostgreSQLCREATE INDEX CONCURRENTLY, for example).
Forward-only migrations (no rollback)
DbUp has no rollback mechanism, and that's intentional. Once data has been modified, automated rollback is usually impossible anyway. The recommended pattern is:
- To undo migration
0042, write a new migration0043-revert-0042.sqlthat contains the reverse SQL. - This creates a linear, auditable history of every schema change.
Zero-downtime schema changes
For high-availability apps, database changes must stay backward-compatible with the currently-running application code for at least one deploy cycle. For zero-downtime deployments:
- Adding a required column: ship it as nullable first, deploy the code that writes to it, then make it
NOT NULLin a later migration. - Renaming: add the new column, dual-write in code, backfill, then drop the old column — three migrations and at least two deploys.
- Dropping: deprecate in code first, wait a release cycle, then drop in migration.
Logging and monitoring
LogToConsole() is fine for local development but thin in production. Use LogTo(ILog) to plug in Serilog, NLog, or Microsoft.Extensions.Logging so migration events flow into your existing log aggregation. For Slack or email notifications on migration success/failure, have the migration console app POST to a webhook before exiting.
Best practices
- Test locally before committing. A syntax error in a migration script is a production incident waiting to happen.
- Keep migrations small. One logical change per file. Makes review and troubleshooting tractable.
- Never modify a migration that's been applied anywhere. Once a script has run in any environment, it's immutable. If you need to change it, write a new script that fixes it forward.
- Include manual rollback notes in comments. DbUp won't execute them, but you or a teammate will thank you at 3am.
- Review migration scripts in PRs. Database changes are code changes — same review process.
- Watch migration duration. Long-running migrations on large tables can exceed deployment windows. Run
EXPLAINbefore shipping anything that touches millions of rows.
Troubleshooting
Migration fails but application still deploys. The migration console returned 0 when it should have returned -1. Check your if (!result.Successful) branch actually returns non-zero, and confirm DeployHQ's command is configured to stop on failure.
Scripts run out of order. DbUp sorts alphabetically. 10-foo.sql runs before 2-bar.sql. Always use zero-padded prefixes (0002-..., 0010-...).
Connection string parsing errors. Special characters (semicolons, quotes) in passwords break parsers. Use DeployHQ's encrypted environment variables or URL-encode the password.
Permission denied on the server. The DeployHQ SSH user needs execute permission on the migration binary and network access to the database. Check both file permissions (chmod +x) and firewall / security group rules.
Conclusion
Pairing DbUp with DeployHQ gives you a predictable, reviewable, automated migration pipeline on .NET 9 — across SQL Server, PostgreSQL, and MySQL alike. Database changes move through the same Git-reviewed, CI-built, deploy-gated pipeline as your application code, and you never ship code that out-runs its schema.
The initial setup is a one-time cost. After that, every deploy includes its migrations by default, and every migration is forward-only, script-tracked, and auditable.
Start with the setup above, then grow the pipeline as your team grows. For hands-on help, get started with DeployHQ or reach out at support@deployhq.com or on X.