RE:NODE

Prisma migrate deploy в продакшене: baseline и восстановление

Автор Команда RE:NODE
13 мин чтения


В продакшене вы запускаете prisma migrate deploy и больше ничего. Команда применяет файлы миграций, уже лежащие в prisma/migrations, по порядку, записывает каждую в таблицу _prisma_migrations и останавливается. prisma migrate dev - инструмент разработки: он создаёт новые миграции, требует вторую одноразовую базу (shadow database) и, обнаружив расхождение, предложит сбросить ваши данные. Направить его на боевую базу - самый частый способ её потерять.

Остальное - детали, и именно в них ломаются деплои: где запускается команда, что делать, если она упала на полпути, как взять под управление базу, в которой уже есть таблицы, и почему в Prisma нет «отмены». В примерах здесь PostgreSQL; MySQL и SQL Server ведут себя так же, кроме оговорённых мест.

Что делают migrate dev и migrate deploy#

Миграции Prisma - это обычные SQL-файлы. Вы правите schema.prisma, Prisma вычисляет разницу с последней миграцией и пишет prisma/migrations/<timestamp>_<name>/migration.sql. Эти файлы коммитятся в Git и служат источником истины. Две команды предназначены для разных ситуаций:

КомандаДля чегоЧто затрагивает
prisma migrate devВаш ноутбукDev-база и shadow-база
prisma migrate deployCI, staging, продакшенТолько целевая база
prisma migrate statusГде угодно, только чтениеЧитает _prisma_migrations
prisma db pushПрототипыСхема, без истории миграций

migrate dev делает пять вещей за раз: прогоняет историю на shadow-базе, чтобы найти расхождения, применяет ожидающие миграции, создаёт новую миграцию, если schema.prisma изменилась, запускает prisma generate и запускает seed-скрипт, если он настроен. Команда интерактивная и задаёт вопросы вроде «нужно сбросить базу, все данные будут потеряны», когда dev-база перестала совпадать с историей.

migrate deploy делает одно. Читает папку, сравнивает с _prisma_migrations и выполняет то, чего не хватает. Она не создаёт миграции, ничего не сбрасывает, не использует shadow-базу, не проверяет расхождения и не запускает generate и seed. В этой узости и смысл: её безопасно запускать без присмотра и безопасно запускать дважды, потому что второй запуск ничего не находит.

bash
$ 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.

Два момента стоит выделить. Во-первых, prisma db push - не облегчённая версия того же. Он синхронизирует схему напрямую, без истории, поэтому последующему migrate deploy не на что опереться. Используйте его для одноразовых прототипов и никогда - на базе, которую собираетесь хранить. Во-вторых, migrate deploy доверяет папке. Если кто-то отредактировал уже применённый migration.sql, Prisma заметит несовпадение контрольной суммы в dev, а deploy просто продолжит работу с тем, что записано. Никогда не правьте миграцию, которая где-то уже применена; добавьте новую.

Shadow database#

Shadow database - временная база, которую migrate dev создаёт, заполняет, прогоняя все миграции с нуля, сравнивает с вашей текущей схемой и снова удаляет. Так он замечает, что кто-то изменил dev-базу вручную, и строит корректную разницу для следующей миграции.

Для хостинга это важно, потому что пользователю базы нужно право создавать базы данных. На управляемой или общей базе такого права обычно нет, и вы видите:

code
Error: P3014Prisma Migrate could not create the shadow database. Please make sure thedatabase user has permission to create databases.

Решение - дать migrate dev вторую базу, указав её в schema.prisma:

prisma/schema.prisma
datasource db {  provider          = "postgresql"  url               = env("DATABASE_URL")  shadowDatabaseUrl = env("SHADOW_DATABASE_URL")}

Один раз создайте для неё пустую базу, направьте SHADOW_DATABASE_URL на неё и используйте только для разработки. Prisma очищает её при каждом запуске. Отсюда два правила:

  • Продакшену shadow-база не нужна. migrate deploy не читает shadowDatabaseUrl. Если ваш боевой деплой жалуется на неё, вы запускаете там migrate dev.
  • Shadow-база не должна быть dev-базой. Если оба URL указывают на одну базу, при первом запуске ваши dev-данные будут стёрты.

В Prisma 7 URL подключения перенесены из schema.prisma в файл prisma.config.ts, поэтому то, где именно объявляются url и shadowDatabaseUrl, зависит от установленной мажорной версии. Поведение описано то же; если schema.prisma отвергает строку url, загляните в справочник по конфигурации для вашей версии.

Настройка боевого запуска#

Рецепт короткий. В CI или на сервере, после установки зависимостей и до старта приложения:

  1. Установите ровно то, что написано в package-lock.json: npm ci.
  2. Сгенерируйте клиент: npx prisma generate. Он не входит в migrate deploy, и приложение, запущенное без него, падает на import { PrismaClient }.
  3. Примените миграции: npx prisma migrate deploy.
  4. Запустите приложение.

Держите prisma в dependencies, а не только в devDependencies, если миграции выполняются на той же машине, что и приложение, с NODE_ENV=production; иначе npm ci --omit=dev удалит CLI, который вы собираетесь вызвать.

package.json
{  "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"  }}

Строка подключения берётся из окружения, а не из закоммиченного .env. Назовите её DATABASE_URL и укажите sslmode, если база требует TLS - какое значение вам нужно, разобрано в sslmode в PostgreSQL. Общий подход к секретам описан в статье переменные окружения и секреты.

Запуск миграций при старте

"start:prod": "npm run migrate && npm run start" - вариант, который выбирает большинство небольших приложений: каждый старт применяет всё ожидающее, а затем начинает обслуживание. У него есть реальные плюсы: не нужно помнить о лишнем шаге, а схема всегда соответствует запускаемому коду. Минусы те же, что у любой схемы «миграции при старте»; статья про миграции EF Core разбирает их на примере .NET:

  • Упавшая миграция - это упавший старт. Процесс завершается с ненулевым кодом, платформа его перезапускает, и он падает снова. Видимый симптом - цикл перезапусков, а причина в нескольких строках выше по журналу.
  • Несколько экземпляров гоняются друг с другом. Во время миграции Prisma берёт advisory lock в PostgreSQL и MySQL, поэтому два одновременно стартующих экземпляра не выполнят одну миграцию дважды: второй ждёт и затем ничего не находит. Если ждать слишком долго, блокировка истекает по таймауту (по умолчанию около десяти секунд), и этот экземпляр не запускается. Это страховка, а не план.
  • Учётные данные приложения могут менять схему. Всё, с чем приложение подключается, может заодно удалять таблицы.
  • Долгие миграции задерживают готовность. Миграция, перестраивающая индекс большой таблицы, выполняется, пока ваш сайт недоступен, ведь приложение ещё не стартовало.

&& важен. С ; приложение запустится, даже если миграция упала, и будет работать со схемой, которой не соответствует. С && сбой останавливает старт и виден сразу.

Для одного экземпляра на одном сервере запуск при старте - разумный выбор. Как только появляются два экземпляра, большая таблица или команда, вынесите миграцию в отдельный шаг: задачу в CI, release-команду или разовый запуск до переключения на новую версию. Последовательность та же, что в деплое без простоя на небольшом сервере, с одной дополнительной строкой перед переключением.

На RE:NODE

На тарифе приложений Node.js миграция входит в последовательность запуска, как и везде: ваша команда запуска выполняет npm run migrate && npm run start, а DATABASE_URL задан среди переменных окружения сервера на вкладке Startup. Тариф PostgreSQL из раздела хостинг баз данных выдаёт хост, порт, пользователя и пароль, сгенерированные для каждого сервера; соберите из них URL и не храните его в репозитории. Если вы деплоите из GitHub, pull при каждом старте приносит новые файлы миграций до выполнения команды - см. деплой Node-приложения из GitHub. Запланируйте backup на вкладке Schedules перед релизом с разрушающей миграцией или сделайте его вручную на вкладке Backups в тарифе сервера приложения.

Baseline существующей базы#

Если в базе уже есть таблицы, но нет таблицы _prisma_migrations, migrate deploy отказывается её трогать:

code
Error: P3005The database schema is not empty. Read more about how to baseline an existingproduction database: https://pris.ly/d/migrate-baseline

Baseline означает сказать Prisma: «в этой базе уже есть всё до миграции X, не запускай её снова». Вы создаёте одну миграцию, описывающую текущее состояние, помечаете её применённой без выполнения, и дальше запускаются только новые.

  1. Приведите schema.prisma в соответствие с живой базой. Если вы подключили Prisma к существующей базе, npx prisma db pull запишет схему из неё. Закоммитьте результат.
  2. Создайте папку baseline. Её имя должно сортироваться раньше всех последующих миграций, подойдёт префикс 0_:
bash
$ 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
  1. Сообщите Prisma, что эта миграция уже применена в целевой базе:
bash
$ npx prisma migrate resolve --applied 0_initMigration 0_init marked as applied.
  1. Выполните npx prisma migrate deploy. Он сообщит, что ожидающих миграций нет. Все добавляемые дальше миграции применяются как обычно.

Флаг --to-schema-datamodel в новых выпусках переименован (замена - --to-schema), поэтому, если ваша версия его отвергает, выполните npx prisma migrate diff --help. Сначала проделайте всё на копии. Восстановите вчерашний дамп в пробную базу, выполните шаги на ней и убедитесь, что npx prisma migrate status пишет «Database schema is up to date!».

Новые окружения - CI, свежая staging-база, новый разработчик - baseline не делают. У них пустая база, поэтому migrate deploy выполняет 0_init по-настоящему, а затем остальные. Поэтому SQL baseline обязан точно описывать схему: если diff пропустил индекс или значение по умолчанию, свежая база и продакшен тихо разойдутся. После baseline сравните их через prisma migrate diff --from-url для боевой базы и --to-schema-datamodel для схемы и ожидайте пустой результат.

Когда миграция падает#

Миграция выполняется как последовательность SQL-операторов, и последствия сбоя зависят от базы. PostgreSQL по возможности оборачивает миграцию в транзакцию, поэтому упавшая миграция обычно не оставляет наполовину применённых изменений. MySQL фиксирует DDL-операторы по ходу, и сбой на середине оставляет применённой первую половину. В любом случае Prisma записывает сбой в _prisma_migrations со временем начала и без времени завершения и отказывается продолжать:

code
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 failed

Каждый следующий migrate deploy упирается в это препятствие, пока вы не разберётесь. Так задумано. Порядок действий:

  1. Прочитайте настоящую ошибку. P3009 лишь сообщает, что раньше что-то упало. Исходное сообщение - P3018 в журнале упавшего запуска, а под ним ошибка SQL. Типичные причины: столбец NOT NULL, добавленный в таблицу с данными, уникальный индекс по дублирующимся данным, внешний ключ на несуществующие строки, таймаут оператора.
  2. Осмотрите базу. Соответствует ли схема замыслу миграции полностью, частично или вообще не тронута? В PostgreSQL обычно не тронута; в MySQL проверьте вручную.
  3. Приведите базу в известное состояние. Либо отмените частичное изменение с помощью SQL, либо доведите его до конца с помощью SQL.
  4. Сообщите Prisma, что вы сделали:
bash
# 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
  1. Исправьте файл миграции, если использовали `--rolled-back`. Править миграцию, которая нигде не применилась успешно, можно: её нигде не применяли. Если на staging она прошла, а на продакшене упала, не правьте её: добавьте новую миграцию с исправляющими действиями и выполните --applied для сломанной после ручного ремонта боевой базы.
  2. Снова выполните npx prisma migrate deploy.

migrate resolve меняет только учёт в _prisma_migrations. Он никогда не выполняет ваш SQL. Поэтому он безопасен и поэтому не может починить схему за вас. Проверить состояние можно командой npx prisma migrate status: она перечисляет применённые, ожидающие и упавшие миграции и завершается с ненулевым кодом, если база не актуальна, - удобный барьер для CI.

Родственное сообщение P3008 говорит, что миграция уже записана как применённая; вы выполнили --applied дважды, это безвредно. P3006 означает, что миграция упала на shadow-базе, то есть историю нельзя воспроизвести с нуля, чаще всего потому, что ранняя миграция зависит от того, что существовало только в исходной базе. Чините это в dev, а не на продакшене.

Откаты: down-миграций нет#

В Prisma Migrate нет down. Миграции идут только вперёд, а migrate resolve --rolled-back - это учёт, а не отмена. Остаются три реальных варианта, в порядке предпочтения:

  • Откат вперёд. Напишите новую миграцию, отменяющую изменение: верните столбец, пересоздайте индекс. Для аддитивных изменений это быстро и честно сохраняет историю.
  • Откатите приложение, а не схему. Если предыдущая версия кода работает и с новой схемой, разверните её заново и не трогайте базу. Это возможно, только если миграция была аддитивной, а ради этого их так и проектируют - см. миграции без простоя о схеме expand и contract.
  • Восстановите backup. Единственный ответ на миграцию, уничтожившую данные. Всё записанное после backup теряется, поэтому это крайняя мера и причина, по которой backup делают до деплоя.

Prisma может сама написать SQL для обратного изменения:

bash
$ npx prisma migrate diff \    --from-schema-datamodel prisma/schema.prisma \    --to-migrations prisma/migrations \    --shadow-database-url "$SHADOW_DATABASE_URL" \    --script

Команда сравнивает два состояния и печатает SQL для перехода из одного в другое. Прочитайте его перед запуском; это отправная точка для ручного отката, а не кнопка. Удалить столбец и вернуть его данные - не одна и та же операция.

Чек-лист деплоя#

Релиз с миграцией должен ответить на эти вопросы до выкатки:

ВопросЗачем
Миграция аддитивная?Старый код работает во время переключения
Блокирует ли она большую таблицу?Сборка индексов и смена типов блокируют запись
Backup есть и восстанавливается?Это единственная отмена
Запускалась ли на копии боевых данных?Ограничения падают на реальных строках, а не на пустых
migrate status чист заранее?Упавшая миграция блокирует деплой

Перед каждым релизом, затрагивающим большие таблицы или добавляющим ограничения, запускайте миграцию на восстановленной копии вчерашнего дампа. Столбец NOT NULL, проходящий на пустой dev-базе, падает на 200 000 строк боевой, и гораздо дешевле выяснить это на пробной базе, чем в цикле перезапусков.

Предпочитайте миграции, безопасные в два шага: добавьте столбец, допускающий NULL, и выкатите, заполните его, затем добавьте ограничение в следующей миграции. SQL, который Prisma генерирует для столбца NOT NULL без значения по умолчанию, - это как раз оператор, падающий на заполненной таблице.

FAQ#

Можно ли запускать prisma migrate dev в продакшене?

Нет. Ему нужна shadow-база, он интерактивный, может предложить сбросить ваши данные и создаёт миграции, которые должны были пройти ревью в Git. Используйте prisma migrate deploy.

Запускает ли prisma migrate deploy команду prisma generate?

Нет. Запускайте npx prisma generate сами, на этапе сборки. Без неё клиент в node_modules отсутствует или устарел, и приложение падает при импорте PrismaClient.

Почему я получаю P3005 на базе, которая уже работает?

Потому что в ней есть таблицы, но нет таблицы _prisma_migrations. Сделайте baseline: создайте миграцию 0_init через prisma migrate diff и пометьте её применённой командой prisma migrate resolve --applied 0_init.

Как отменить последнюю миграцию Prisma?

Никак. Напишите новую миграцию, которая её отменяет, или восстановите backup. migrate resolve --rolled-back меняет только записи Prisma об упавшей миграции, а не ваши таблицы.

Безопасно ли запускать migrate deploy сразу из двух экземпляров?

В основном да. Prisma берёт advisory lock в PostgreSQL и MySQL, поэтому второй экземпляр ждёт, а не повторяет работу, но ожидание может истечь по таймауту. Запускайте миграции одним шагом до старта экземпляров.

Где хранится история миграций?

В двух местах, которые обязаны совпадать: в папке prisma/migrations в Git и в таблице _prisma_migrations каждой базы. migrate status показывает, где они расходятся.

Это было полезно?