Skip to main content
友田 陽大
Prisma ORM
Prisma
TypeScript
PostgreSQL
CI/CD
信頼性

Prisma Migrate production-operations guide: the correct dev/deploy separation, shadow DB, baselining an existing DB, zero-downtime expand-and-contract migration, and CI/CD

An implementation guide to safely operating Prisma Migrate (v7) in production. Faithful to the official docs, with real commands it explains the role separation of migrate dev/deploy/reset/diff/resolve, how the shadow DB works, retrofitting onto an existing DB (db pull + baseline), zero-downtime expand-and-contract schema changes that edit SQL with --create-only, drift detection and recovery, migrate deploy in CI/CD, and v7's seed changes.

Published
Reading time
11 min read
Author
友田 陽大
Share

A schema change against a production database carries a different level of tension from an app deploy. A code bug can be rolled back, but a migration that breaks data sometimes can't. "I dropped a column and the data disappeared," "I ran migrate dev in production and the DB got reset" — these accidents are 100% preventable if you understand the commands' roles.

This article is an implementation guide to safely operating Prisma Migrate (v7) in production. Schema design itself is in the complete Prisma schema-design & relations guide, and the Prisma big picture is in the Prisma ORM production-operations guide (v7). This article concentrates on "how to keep applying a designed schema to production without breaking data."

Rules for this article: commands and behavior are based on the Prisma official documentation (as of June 2026, the v7 family). The shadow DB / seed configuration via prisma.config.ts, mandatory driver adapters, and the abolition of automatic seeding are all v7 behavior. Never run destructive operations (reset, etc.) in production, and always confirm the latest specs in the official documentation before production rollout.


0. Mental model: a migration is "SQL to be reviewed"

What I was thorough about operating the payment foundation is the discipline of "a human reviews the auto-generated thing before passing it to production." Prisma Migrate is convenient, but the SQL migrate dev generates isn't omnipotent. Unintended destruction can occur, like interpreting a column rename as "drop + add" and discarding the data.

So the center of the production flow is this.

migrate dev in development to generate SQL → a human reviews the generated SQL (any destructive change?) → commit → CI applies it to production with migrate deploy.

A migration file is "a first-class artifact like code." Commit prisma/migrations/, review in a PR, and apply in CI. Not straying from this single road becomes the greatest line of defense for the production DB.


1. The commands' role separation (confuse this and you'll have an accident)

# 開発専用:差分からマイグレーションを生成して即適用(+クライアント再生成)
npx prisma migrate dev --name add_user_role

# 開発専用:SQLだけ生成して適用しない(手編集したいとき)
npx prisma migrate dev --create-only

# 本番/CI:保留中のマイグレーションだけを順に適用
npx prisma migrate deploy

# 開発専用:DBを破棄→再作成→全マイグレーション再適用(データ消滅)
npx prisma migrate reset

# どの環境でも:適用状況のレポート(CIゲートに使える)
npx prisma migrate status

# 2つのスキーマ差分をSQL/サマリで出力
npx prisma migrate diff --from-empty --to-schema prisma/schema.prisma --script

# _prisma_migrations に「適用済み/ロールバック済み」を記録
npx prisma migrate resolve --applied 20260626120000_init

Fix the roles in a table. Memorize this and accidents drop drastically.

CommandEnvironmentImportant property
migrate devdevelopment onlyneeds a shadow DB. On drift detection, can reset the DB (data loss). Must not be run in production
migrate deployproduction/CIapplies only pending migrations. No drift inspection, no reset, no shadow DB. Warns if an applied one was altered
migrate resetdevelopment onlydrops and recreates the DB (data loss). Strictly forbidden in production
migrate statusanyreturns the application state. Usable as a CI gate since it exits non-zero on unapplied/divergent/failed
migrate diffanythe diff of 2 sources. --script for executable SQL, --exit-code returns 2 on diff
migrate resolveany (the key to production recovery)manually record a migration as "applied/rolled back"

The official explicitly says of migrate deploy that it "inspects neither drift nor schema changes / neither resets nor generates the DB." That's exactly why it's safe in production. Conversely, migrate dev is clearly stated to be "a development command and must not be used in a production environment."


2. Shadow DB: a mechanism to "notice before it breaks" in development

migrate dev, before applying a migration to the real DB, detects drift and potential data loss with a temporary shadow DB. This is automatically created and deleted on each migrate dev run (not needed in production, not used by migrate deploy / migrate resolve).

In v7, you specify the shadow DB's connection target in prisma.config.ts as needed.

// prisma.config.ts
import "dotenv/config";
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  datasource: {
    url: env("DATABASE_URL"),
    shadowDatabaseUrl: env("SHADOW_DATABASE_URL"), // クラウドDB等で手動指定
  },
});

Critical caution (official warning): never specify the same value for url and shadowDatabaseUrl. Because the shadow DB is reset, pointing it at the production DB destroys data. The shadow DB is exclusively a "throwaway DB you may create and break."


3. Retrofitting onto an existing DB: baselining

When introducing Prisma Migrate to "a DB already running in production," running migrate dev straight away collides by trying to recreate existing tables. What prevents this is baselining — the procedure to "record the current state as the starting point of the migration history."

# 1. 既存DBをイントロスペクトして schema.prisma を生成(既存schemaは上書きされる)
npx prisma db pull

# 2. 初期マイグレーション用ディレクトリを用意し、現状を再現するSQLを生成
mkdir -p prisma/migrations/0_init
npx prisma migrate diff \
  --from-empty \
  --to-schema prisma/schema.prisma \
  --script > prisma/migrations/0_init/migration.sql

# 3. その初期マイグレーションを「適用済み」として記録(実行はしない)
npx prisma migrate resolve --applied 0_init

The point is that step 3 doesn't actually run the SQL; it only records "applied" in the _prisma_migrations table. This lets you start the migration history without changing the existing DB at all, and subsequent changes pile up as usual with migrate devmigrate deploy. Always commit the generated schema.prisma and migrations to version control.

Note: migrate diff's flag names can shift by CLI version (the --to-schema family). Confirm your environment's exact flags with npx prisma migrate diff --help before running. MongoDB is out of scope for this procedure (use db push).


4. Zero-downtime schema change: expand-and-contract

While access keeps flowing in production, changing an existing column "all at once" causes a mismatch between new/old code and schema in the instant of a deploy, becoming downtime or errors. Prisma's official documentation explicitly recommends the expand-and-contract pattern to avoid this.

The idea is 3 stages.

  1. Expand: add a new column (leave the old one). The app writes to both new and old.
  2. Data migration (backfill): copy the old column's values to the new column (custom SQL).
  3. Contract: switch the app to read only the new column, and after it stabilizes, drop the old column.

4.1 Rename a column "without breaking data"

migrate dev often generates a column rename as "DROP COLUMN + ADD COLUMN" — this discards the data. Generate with --create-only, hand-edit, and fix it into a RENAME.

-- ❌ 自動生成されがちな破壊的SQL(データ消滅)
ALTER TABLE "Profile" DROP COLUMN "biograpy",
ADD COLUMN "biography" TEXT NOT NULL;

-- ✅ 手編集してデータを保持する
ALTER TABLE "Profile" RENAME COLUMN "biograpy" TO "biography";

4.2 Insert backfill SQL

Write the sequence of adding a new column and copying from the old one as custom SQL in an empty migration.

-- Expand:新列を追加(まずは NULL 許容で)
ALTER TABLE "User" ADD COLUMN "profileId" INTEGER;

-- バックフィル:既存データを新列へ
UPDATE "User"
SET "profileId" = "Profile".id
FROM "Profile"
WHERE "User".id = "Profile"."userId";

-- 安定後(別マイグレーション)に NOT NULL 化/旧列削除(Contract)
ALTER TABLE "User" ALTER COLUMN "profileId" SET NOT NULL;

The official says "this approach lets you avoid the downtime that changing an existing field tends to invite." The point is to deploy in separate stages (make expand → migration → contract separate releases).


5. --create-only: dangerous DDL goes "generate → review → apply"

For risky changes (rename, backfill, adding a NOT NULL column to a huge table, enabling an extension, etc.), always separate generation and application.

# 1. 適用せずにSQLだけ生成
npx prisma migrate dev --create-only

# 2. prisma/migrations/<timestamp>_*/migration.sql を手で編集・レビュー

# 3. 編集済みSQLを適用
npx prisma migrate dev

These 3 steps are the concrete means of realizing in Prisma "a human inspects the auto-generated before passing it" that I keep in production. Adding a NOT NULL column to a huge table tends to hold a write lock for a long time, so on PostgreSQL, stage it as "add NULL-allowed → backfill → SET NOT NULL" and, if needed, also use lock_timeout or CONCURRENTLY — such zero-downtime DDL know-how is reflected into the generated SQL at this review stage.


6. CI/CD: automate deploy and shut out dev/reset

The official guidance is clear. "migrate deploy should be part of the CI/CD pipeline, and flowing production-DB changes from local is not recommended." And "migrate dev is a development command and must not be used in production."

The CI pre-deploy step is conceptually the following form (write the YAML to fit each CI).

# CI/CD(本番/ステージングDBに対して)
npx prisma migrate deploy
# アプリのビルド前に型付きクライアントも生成
npx prisma generate

Three iron rules of operation.

  • Production DB credentials from environment variables (CI secrets). Hardcoding / log output is strictly forbidden.
  • Make migrate status a gate: it exits non-zero on unapplied/divergent, so it's usable as a pre-deploy check.
  • Design the order of migration application and app deploy: in expand-and-contract, separate "schema expand → deploy new code → contract" into separate steps.

7. Recovering from drift and failure

Drift is "a state where the DB's actual state diverged from the migration history (the source of truth)." Manual production hotfixes or a failed migration are the cause. migrate dev detects this with the shadow DB and, in development, may prompt a reset, but you must not reset in production. Production recovery is done with migrate resolve.

# 失敗したマイグレーションを「ロールバック済み」と記録し、修正版を deploy し直す
npx prisma migrate resolve --rolled-back 20260626130000_failed_migration

# 手動で適用を完了させた変更を「適用済み」と記録する
npx prisma migrate resolve --applied 20260626140000_manually_completed

Typical migration failures are syntax errors, adding a required column to a table with data, abnormal process termination, and the DB stopping during application. Each failure is recorded in the logs column of _prisma_migrations. Because migrate deploy doesn't detect drift, defend against production drift with both wheels of "operation that doesn't cause it (forbid direct changes)" and "tidy the history with resolve if it happens."


8. Seed: v7 changes

In v7, the seed configuration moved to migrations.seed in prisma.config.ts, and further, automatic seeding was abolished.

// prisma.config.ts
export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
    seed: "tsx prisma/seed.ts", // db seed が実行するコマンド
  },
});
// prisma/seed.ts(v7:driver adapter 必須)
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "./generated/prisma/client";

const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });

async function main() {
  await prisma.user.upsert({
    where: { email: "admin@example.com" },
    update: {},
    create: { email: "admin@example.com", name: "Admin" },
  });
}

main().finally(() => prisma.$disconnect());
npx prisma db seed

The official breaking change (summary): "In v7, the seed starts only via the explicit npx prisma db seed. The automatic seeding on migrate dev / migrate reset was abolished." The iron rule is to write seeds idempotently with upsert (the same result no matter how many times you run it).


9. Production-migration checklist

  • Development is migrate dev, production/CI is migrate deploy. Never run dev/reset in production
  • Review the generated migration SQL in a PR before merging (check for destructive changes)
  • Risky DDL is generated with --create-only → hand-edited → applied
  • Changes to existing columns are staged-deployed with expand-and-contract (rename hand-edited to RENAME)
  • Existing-DB introduction is baselined with db pullmigrate diff --from-emptymigrate resolve --applied
  • The shadow DB's connection target is separate from production (same URL strictly forbidden)
  • In CI, migrate deploygenerate, with migrate status as a gate
  • Recover production drift/failure with migrate resolve (--rolled-back / --applied). Direct changes forbidden
  • Seeds go in migrations.seed of prisma.config.ts, idempotent with upsert. v7 is explicit db seed only

Conclusion

Safe operation of Prisma Migrate, taken to the limit, boils down to two points: "never confuse the commands' roles" and "a human reviews the auto-generated SQL before passing it to production." Development is migrate dev, production is migrate deploy. Baseline an existing DB, do zero-downtime changes with expand-and-contract, review dangerous DDL with --create-only, and recover with resolve — keep just these and you can keep applying schema changes to the production DB without breaking it.

"I want to build a mechanism to keep flowing schema changes to production without downtime and without breaking data" — from designing migration operations to building it into CI/CD, I can help land it in a form that doesn't break down in production. I apply the know-how that supported 0 double charges and zero-downtime deploys on the payment foundation to your DB operation too.

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading