In production you run prisma migrate deploy and nothing else. It applies the migration files already committed in prisma/migrations, in order, records each in a table called _prisma_migrations, and stops. prisma migrate dev is a development tool: it creates new migrations, needs a second throwaway database (the shadow database), and will offer to reset your data when it finds drift. Pointing it at a production database is the single most common way people lose one.
The rest is detail, and the detail is where deploys fail: where the command runs, what to do when it fails halfway, how to adopt a database that already has tables, and why Prisma has no "undo". This post uses PostgreSQL in the examples; MySQL and SQL Server behave the same except where noted.
What migrate dev and migrate deploy each do#
Prisma migrations are plain SQL files. You edit schema.prisma, Prisma works out the difference from the last migration, and writes prisma/migrations/<timestamp>_<name>/migration.sql. Those files are committed to Git and are the source of truth. The two commands differ in who they are for:
| Command | Meant for | Touches |
|---|---|---|
prisma migrate dev | Your laptop | Dev database and shadow database |
prisma migrate deploy | CI, staging, production | Target database only |
prisma migrate status | Anywhere, read-only | Reads _prisma_migrations |
prisma db push | Prototyping | Schema, no migration history |
migrate dev does five things in one go: it replays the history on a shadow database to detect drift, applies pending migrations, generates a new migration if schema.prisma changed, runs prisma generate, and runs your seed script if one is configured. It is interactive, and it asks questions like "We need to reset the database, all data will be lost" when the dev database no longer matches the history.
migrate deploy does one thing. It reads the folder, compares it with _prisma_migrations, and runs whatever is missing. It does not create migrations, does not reset anything, does not use a shadow database, does not check for drift and does not run generate or the seed. That narrowness is the point: it is safe to run unattended, and it is safe to run twice, because the second run finds nothing pending.
$ npx prisma migrate deployPrisma schema loaded from prisma/schema.prismaDatasource "db": PostgreSQL database "shop", schema "public" at "db.internal:5432"3 migrations found in prisma/migrationsApplying migration `20261012093011_add_orders`The following migration(s) have been applied:migrations/ └─ 20261012093011_add_orders/ └─ migration.sqlAll migrations have been successfully applied.Two things deserve emphasis. First, prisma db push is not a lighter version of the same thing. It syncs the schema directly with no history, so a later migrate deploy has nothing consistent to build on. Use it for throwaway prototypes and never on a database you intend to keep. Second, migrate deploy trusts the folder. If someone edited an already-applied migration.sql, Prisma notices the checksum mismatch in dev but deploy simply carries on with what is recorded. Never edit a migration that has been applied anywhere; add a new one.
The shadow database#
The shadow database is a temporary database that migrate dev creates, fills by replaying every migration from the start, compares against your current schema, and drops again. That is how it detects that someone changed the dev database by hand, and how it produces a correct diff for the next migration.
It matters for hosting because the database user must be allowed to create databases. On a managed or shared database that user usually cannot, and you see:
Error: P3014Prisma Migrate could not create the shadow database. Please make sure thedatabase user has permission to create databases.The fix is to give migrate dev a second database to use, named in schema.prisma:
datasource db { provider = "postgresql" url = env("DATABASE_URL") shadowDatabaseUrl = env("SHADOW_DATABASE_URL")}Create an empty database for it once, point SHADOW_DATABASE_URL at it, and keep it for development only. Prisma empties it for each run. Two rules follow from this:
- Production never needs a shadow database.
migrate deploydoes not readshadowDatabaseUrl. If your production deploy complains about one, you are runningmigrate devthere. - The shadow database must not be the dev database. Pointing both URLs at the same database wipes your dev data on the first run.
Prisma 7 moved the connection URLs out of schema.prisma into a prisma.config.ts file, so the exact place you declare url and shadowDatabaseUrl depends on the major version you installed. The behaviour described here is the same; check the configuration reference for your version if schema.prisma rejects the url line.
Setting up the production run#
The recipe is short. In CI or on the server, after dependencies are installed and before the app starts:
- Install exactly what
package-lock.jsonsays:npm ci. - Generate the client:
npx prisma generate. It is not part ofmigrate deploy, and an app started without it fails onimport { PrismaClient }. - Apply migrations:
npx prisma migrate deploy. - Start the app.
Put prisma itself in dependencies, not only devDependencies, if migrations run on the same machine as the app with NODE_ENV=production; otherwise npm ci --omit=dev removes the CLI you are about to call.
{ "scripts": { "build": "prisma generate && tsc", "migrate": "prisma migrate deploy", "start": "node dist/server.js", "start:prod": "npm run migrate && npm run start" }, "dependencies": { "@prisma/client": "^6.0.0", "prisma": "^6.0.0" }}The connection string comes from the environment, never from a committed .env. Keep it as DATABASE_URL, with sslmode stated if your database requires TLS - PostgreSQL sslmode explained covers which value you want. The general handling of secrets is in environment variables and secrets.
Running migrations on start
"start:prod": "npm run migrate && npm run start" is the option most small apps use: every start applies whatever is pending, then serves. It has real advantages - there is no step to forget, and the schema always matches the code that is booting. The costs are the same ones you meet with any migrate-on-start setup, and the EF Core migrations post walks through them in .NET terms:
- A failed migration is a failed start. The process exits non-zero, the platform restarts it, and it fails again. The visible symptom is a restart loop; the cause is a few lines up in the log.
- Several instances race. Prisma takes an advisory lock on PostgreSQL and MySQL while migrating, so two instances starting together do not run the same migration twice: the second waits, then finds nothing to do. If it waits too long the lock times out (about ten seconds by default) and that instance fails to start. It is a safety net, not a plan.
- The app's credentials can change the schema. Everything the app connects with can also drop tables.
- Slow migrations delay readiness. A migration that rebuilds an index on a large table runs while your site is down, because the app has not started yet.
The && matters. With ; the app starts even when the migration failed, and then runs against a schema it does not match. With && a failure stops the start and is visible.
For a single instance on a single server, running on start is a reasonable choice. As soon as you have two instances, a large table, or a team, move the migration into its own step: a CI job, a release command, or a one-off run before the new version is switched in. The sequence is the same as the one in zero-downtime deploys on a small server, with one extra line before the switch.
On RE:NODE
On a Node.js app plan the migration is part of the start sequence like anywhere else: your start command runs npm run migrate && npm run start, with DATABASE_URL set among the server's environment variables on the Startup tab. A PostgreSQL plan from database hosting gives you a host, port, user and password generated per server; build the URL from those and keep it out of the repository. If you deploy from GitHub, the pull on every start brings in new migration files before the command runs - see deploying a Node app from GitHub. Schedule a backup from the Schedules tab before a release that includes a destructive migration, or take one by hand from the Backups tab on the app server's own plan.
Baselining an existing database#
If the database already has tables and no _prisma_migrations table, migrate deploy refuses to touch it:
Error: P3005The database schema is not empty. Read more about how to baseline an existingproduction database: https://pris.ly/d/migrate-baselineBaselining means telling Prisma "this database already contains everything up to migration X; do not run it again". You create one migration that describes the current state, mark it as applied without running it, and from then on only new migrations run.
- Make
schema.prismamatch the live database. If you adopted Prisma on an existing database,npx prisma db pullwrites the schema from it. Commit that. - Create the baseline folder. Its name must sort before every later migration, so a leading
0_works:
$ mkdir -p prisma/migrations/0_init$ npx prisma migrate diff \ --from-empty \ --to-schema-datamodel prisma/schema.prisma \ --script > prisma/migrations/0_init/migration.sql- Tell Prisma that migration is already applied on the target database:
$ npx prisma migrate resolve --applied 0_initMigration 0_init marked as applied.- Run
npx prisma migrate deploy. It reports no pending migrations. Every migration you add afterwards is applied normally.
The flag --to-schema-datamodel was renamed in newer releases (the replacement is --to-schema), so run npx prisma migrate diff --help if your version rejects it. Do this on a copy first. Restore last night's dump into a scratch database, run the steps against it, and check that npx prisma migrate status says "Database schema is up to date!".
New environments - CI, a fresh staging database, a new developer - do not baseline. They have an empty database, so migrate deploy runs 0_init for real and then the rest. That is why the baseline SQL has to be a faithful description of the schema: if the diff missed an index or a default, a fresh database and production quietly differ. After baselining, compare the two with prisma migrate diff --from-url against production and --to-schema-datamodel against the schema, and expect an empty result.
When a migration fails#
A migration runs as a series of SQL statements, and what happens when one fails depends on the database. PostgreSQL wraps a migration in a transaction where it can, so a failed migration normally leaves no half-applied changes. MySQL commits DDL statements as it goes; a failure part-way leaves the first half applied. Either way, Prisma records the failure in _prisma_migrations with a started time and no finished time, and refuses to continue:
Error: P3009migrate found failed migrations in the target database, new migrations willnot be applied. Read more about how to resolve migration issues in aproduction database: https://pris.ly/d/migrate-resolveThe `20261012093011_add_orders` migration started at 2026-10-12 09:30:11 UTC failedEvery later migrate deploy hits this wall until you deal with it. That is deliberate. The procedure:
- Read the real error.
P3009only says something failed earlier. The original message isP3018in the log of the failed run, with the SQL error beneath it. Typical causes: aNOT NULLcolumn added to a table that already has rows, a unique index over duplicate data, a foreign key to rows that do not exist, a statement timeout. - Inspect the database. Is the schema what the migration intended, partly, or untouched? On PostgreSQL it is usually untouched; on MySQL check by hand.
- Put the database in a known state. Either undo the partial change with SQL, or finish it with SQL.
- Tell Prisma what you did:
# the migration did not apply (or you undid it): it will run again$ npx prisma migrate resolve --rolled-back 20261012093011_add_orders# you completed its effect by hand: never run it$ npx prisma migrate resolve --applied 20261012093011_add_orders- Fix the migration file if you used `--rolled-back`. Editing a migration that failed everywhere is fine, because it was never applied successfully anywhere. If it succeeded on staging and failed on production, do not edit it: add a new migration that does the corrective work and run
--appliedon the broken one after repairing production by hand. - Run
npx prisma migrate deployagain.
migrate resolve changes only the bookkeeping in _prisma_migrations. It never runs your SQL. That is why it is safe, and why it cannot fix a schema for you. Check the state with npx prisma migrate status, which lists applied, pending and failed migrations and exits non-zero if the database is not up to date - a useful gate in CI.
A related message, P3008, says a migration is already recorded as applied; you have run --applied twice, which is harmless. P3006 says a migration failed on the shadow database, which means the history cannot be replayed from empty, most often because an early migration depends on something that existed only in the original database. Fix that in dev, not production.
Rollbacks: there is no down migration#
Prisma Migrate has no down. Migrations move forward only, and migrate resolve --rolled-back is bookkeeping, not an undo. You have three real options, in order of preference:
- Roll forward. Write a new migration that reverses the change: add the column back, recreate the index. For additive changes, this is quick and keeps the history honest.
- Roll back the app, not the schema. If the previous version of the code still works against the new schema, redeploy that version and leave the database alone. This is only possible when the migration was additive, which is the point of designing them that way - see migrations without downtime for the expand and contract pattern.
- Restore a backup. The only answer to a migration that destroyed data. Everything written since the backup is lost, so this is the last resort and the reason the backup comes before the deploy.
You can ask Prisma to write the reversing SQL for you:
$ npx prisma migrate diff \ --from-schema-datamodel prisma/schema.prisma \ --to-migrations prisma/migrations \ --shadow-database-url "$SHADOW_DATABASE_URL" \ --scriptThis compares two states and prints the SQL that goes from one to the other. Read it before running it; it is a starting point for a hand-written rollback, not a button. Dropping a column and getting its data back are not the same operation.
A deploy checklist#
A release that includes a migration should answer these before it ships:
| Question | Why |
|---|---|
| Is the migration additive? | Old code keeps running during the switch |
| Does it lock a large table? | Index builds and type changes block writes |
| Does the backup exist and restore? | It is the only undo |
| Has it run on a copy of production data? | Constraints fail on real rows, not empty ones |
Is migrate status clean beforehand? | A failed one blocks the deploy |
Run the migration against a restored copy of last night's dump before every release that touches large tables or adds constraints. A NOT NULL column that passes on an empty dev database fails on the 200,000 rows production holds, and it is much cheaper to find that out on a scratch database than in a restart loop.
Prefer migrations that are safe in two steps: add a nullable column and deploy, backfill, then add the constraint in a later migration. The SQL Prisma generates for a NOT NULL column without a default is exactly the statement that fails on a populated table.
FAQ#
Can I run prisma migrate dev in production?
No. It needs a shadow database, is interactive, can propose to reset your data, and generates migrations that should have been reviewed in Git. Use prisma migrate deploy.
Does prisma migrate deploy run prisma generate?
No. Run npx prisma generate yourself, in your build step. Without it the client in node_modules is missing or stale and the app fails when it imports PrismaClient.
Why do I get P3005 on a database that already works?
Because it has tables but no _prisma_migrations table. Baseline it: create an 0_init migration with prisma migrate diff, then mark it applied with prisma migrate resolve --applied 0_init.
How do I undo the last Prisma migration?
You do not. Write a new migration that reverses it, or restore a backup. migrate resolve --rolled-back only changes Prisma's records of a failed migration, not your tables.
Is it safe to run migrate deploy from two instances at once?
Mostly. Prisma takes an advisory lock on PostgreSQL and MySQL, so the second instance waits rather than repeating the work, but the wait can time out. Run migrations as a single step before the instances start.
Where does the migration history live?
In two places that must agree: the prisma/migrations folder in Git, and the _prisma_migrations table in each database. migrate status shows where they differ.


