# 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: 2026-06-26
- Author: 友田 陽大
- Tags: Prisma, TypeScript, PostgreSQL, CI/CD, 信頼性
- URL: https://tomodahinata.com/en/blog/prisma-migrate-production-zero-downtime-cicd-guide
- Category: Prisma ORM
- Pillar guide: https://tomodahinata.com/en/blog/prisma-orm-production-guide-type-safe-database-v7-driver-adapters

## Key points

- Never confuse the commands' roles. Development is migrate dev (shadow DB required, can reset); production CI is migrate deploy (applies only pending, no drift inspection, no reset).
- Retrofitting onto an existing DB is baselining. Pull it in with db pull, generate the initial SQL with migrate diff --from-empty, and record it as applied with migrate resolve --applied.
- Zero-downtime migration is the 3 stages of expand-and-contract. Add column → dual-write → backfill → switch to the new column → drop the old column. For rename, hand-edit the auto-generated DROP+ADD into a RENAME.
- For risky DDL, generate the SQL with migrate dev --create-only → a human reviews/edits → apply. Always commit the generated SQL and make it a review target.
- Resolve production drift/failure with migrate resolve (--rolled-back/--applied). migrate dev/reset in production is strictly forbidden. v7 abolished automatic seeding, only explicit db seed.

---

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](/blog/prisma-schema-data-modeling-relations-design-guide), and the Prisma big picture is in the [Prisma ORM production-operations guide (v7)](/blog/prisma-orm-production-guide-type-safe-database-v7-driver-adapters). 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](https://www.prisma.io/docs/orm/prisma-migrate/workflows/development-and-production) 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)

```bash
# 開発専用：差分からマイグレーションを生成して即適用（＋クライアント再生成）
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.**

| Command | Environment | Important property |
| --- | --- | --- |
| `migrate dev` | **development only** | needs a shadow DB. On drift detection, **can reset the DB (data loss).** Must not be run in production |
| `migrate deploy` | **production/CI** | **applies only pending migrations.** No drift inspection, no reset, no shadow DB. Warns if an applied one was altered |
| `migrate reset` | **development only** | drops and recreates the DB (data loss). Strictly forbidden in production |
| `migrate status` | any | returns the application state. Usable as a CI gate since it **exits non-zero** on unapplied/divergent/failed |
| `migrate diff` | any | the diff of 2 sources. `--script` for executable SQL, `--exit-code` returns 2 on diff |
| `migrate resolve` | any (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.

```ts
// 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."

```bash
# 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 dev` → `migrate 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
-- ❌ 自動生成されがちな破壊的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.

```sql
-- 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.**

```bash
# 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).

```bash
# 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`.

```bash
# 失敗したマイグレーションを「ロールバック済み」と記録し、修正版を 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.**

```ts
// prisma.config.ts
export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
    seed: "tsx prisma/seed.ts", // db seed が実行するコマンド
  },
});
```

```ts
// 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());
```

```bash
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 pull` → `migrate diff --from-empty` → `migrate resolve --applied`
- [ ] The shadow DB's connection target is **separate** from production (same URL strictly forbidden)
- [ ] In CI, `migrate deploy` → `generate`, 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.
