 **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](https://www.deployhq.com/features/build-pipelines) 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:

1. **Full SQL control** — you can use provider-specific features (`SQL Server OPTIMIZE FOR UNKNOWN`, Postgres `CREATE INDEX CONCURRENTLY`, MySQL `ALGORITHM=INPLACE`) without fighting an ORM.
2. **Reviewable diffs** — migrations are plain `.sql` files your DBA can read without running the tool.
3. **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:

1. You push to your Git repository ([GitHub](https://www.deployhq.com/deploy-from-github), [GitLab](https://www.deployhq.com/deploy-from-gitlab), Bitbucket, etc.)
2. [DeployHQ](https://www.deployhq.com) detects the change and triggers a deployment
3. Your application is built and deployed to your server
4. [DeployHQ](https://www.deployhq.com) runs a post-deploy command that executes your DbUp migration console app
5. DbUp reads the embedded scripts, compares to `SchemaVersions`, runs the ones that haven't run
6. The deployment is marked complete only if DbUp exits with code `0`

If DbUp fails, [DeployHQ](https://www.deployhq.com) halts the deployment — so application code never ships ahead of its matching schema. This is the whole point of [automated deployment](https://www.deployhq.com/features/automatic-deployments).

## 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.sql`
- `0002-AddEmailIndexToUsers.sql`
- `0003-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](https://www.deployhq.com) integration — it's the signal that tells [DeployHQ](https://www.deployhq.com) 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](https://www.deployhq.com) and create a new project pointing at your Git repository. Connect your deployment target — SFTP, SSH, Amazon S3, or a custom [SSH server](https://www.deployhq.com/blog/how-to-deploy-to-your-server-using-ssh-sftp-and-git-with-deployhq). 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](https://www.deployhq.com) 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](https://www.deployhq.com) command strings.

### Step 11: Run the first deployment

Commit a change (or trigger a manual deploy) and watch the log:

1. Code pulled from the repository
2. `dotnet publish` runs in the build pipeline
3. Files transferred to the server
4. Migration command runs, DbUp logs each script it applies
5. Deployment marked successful — or halted if DbUp exited non-zero

## Advanced configuration

### Per-environment deployments

Create a separate [DeployHQ](https://www.deployhq.com) 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](https://www.deployhq.com/blog/deploying-to-multiple-environments-staging-vs-production).

### 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 Server `CREATE INDEX WITH ONLINE=ON`, for example).
- **`WithoutTransaction()`** — required for operations that explicitly forbid transactions (PostgreSQL `CREATE 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 migration `0043-revert-0042.sql` that 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](https://www.deployhq.com/features/zero-downtime-deployments):

- Adding a required column: ship it as nullable first, deploy the code that writes to it, then make it `NOT NULL` in 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 `EXPLAIN` before 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](https://www.deployhq.com/blog/how-to-deploy-php-applications-with-encrypted-environment-variables-using-dotenvx-and-deployhq) or URL-encode the password.

**Permission denied on the server.** The [DeployHQ](https://www.deployhq.com) 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](https://www.deployhq.com) 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](https://www.deployhq.com/signup) or reach out at [support@deployhq.com](mailto:support@deployhq.com) or on [X](https://x.com/deployhq).

