Большинство неудачных деплоев ломаются не на самом деплое. Код скачан, зависимости установлены, контейнер запущен, а потом приложение завершается: у него нет скрипта запуска, или оно слушает порт 3000, до которого снаружи не достучаться, или ищет папку .next, которую никто не собирал. Каждая из этих ошибок видна в репозитории ещё до того, как развёрнут хоть один байт. Pre-flight check на вкладке GitHub в панели читает репозиторий, сравнивает его с тем, как сервер будет его запускать, и перечисляет то, что сломается, начиная с худшего, и к каждой находке даёт исправление, которое можно скопировать.
В этой статье - что именно читает проверка, как читать карточку, какие классы проблем она находит (с реальным примером конфигурации, которая их вызывает) и как исправить каждую. Также разберём, чего проверка увидеть не может, потому что зелёная карточка не гарантирует, что приложение работает. Большинство исправлений подходят для любого хостинга: жёстко прописанный порт или привязка к localhost ломают деплой везде, а не только на RE:NODE.
Что проверка читает и чего никогда не трогает#
Проверка работает с двумя источниками: одним рекурсивным списком дерева ветки и коротким набором небольших файлов, которые запрашиваются по имени. Список дерева - это только пути и размеры, без содержимого. Читать она может файлы, от которых зависит запуск приложения: package.json, deno.json или deno.jsonc, go.mod и Go-файлы главного пакета, requirements.txt или pyproject.toml, .nvmrc, .env.example и входной файл, который запустит сервер. Репозиторий она не клонирует и пропускает любой файл крупнее 256 КБ, потому что ничего важного для запуска в таких файлах нет.
Проверка запускается в двух местах. Когда вы выбираете репозиторий и ветку на вкладке GitHub, она срабатывает до сохранения связи, так что вы видите проблемы раньше, чем что-либо развёрнуто. Дальше она запускается заново при каждом открытии вкладки. Результаты кэшируются по коммиту на десять минут, но состояние головы ветки запрашивается каждый раз, поэтому только что сделанный push замечается сразу, а кэш лишь избавляет от повторного чтения неизменившихся файлов.
Три вещи, которых она намеренно не делает:
- Она никогда не открывает `.env`. О закоммиченном
.envсообщается только по имени файла. Его содержимое не запрашивается, поэтому ничего из него не может попасть в находки. - Она никогда не цитирует файл целиком. Результат - список находок: предложения, написанные панелью, где в лучшем случае цитируется скрипт или имя переменной. Любая строка проходит через тот же редактор секретов, что и ИИ-функции, поэтому скрипт запуска с вписанным паролем будет процитирован без пароля.
- Она никогда не падает с шумом. GitHub недоступен, превышен лимит запросов, GitHub App удалён, ветки пока не существует: каждый такой случай превращается в одну заметку о том, что проверку выполнить не удалось. Проверка, которая роняет всю вкладку, хуже, чем её отсутствие.
Проверка следует той же логике запуска, что и сервер. У неё нет отдельного представления о том, как должно выглядеть приложение на Node или Go: она читает список входных файлов, к которым откатывается стартовый скрипт, и переменную, которую вкладка Startup использует для каждого рантайма. Когда меняется запуск, меняется и проверка.
Как читать карточку pre-flight#
Карточка находится на вкладке GitHub под заголовком "Pre-flight check", со строкой о том, какой коммит и какая ветка прочитаны. У каждой находки один из четырёх уровней, и список отсортирован с худшим сверху:
| Уровень | Метка на карточке | Что это значит |
|---|---|---|
| fail | Will break | Приложение не запустится, либо запустится, но будет недоступно |
| warn | Worth fixing | Оно будет работать, но плохо или не так, как задумано |
| info | Good to know | Контекст или то, что проверка не смогла определить |
| pass | Fine | Эта часть соответствует тому, как сервер запускает приложение |
У каждой находки есть заголовок, предложение с пояснением и - где это возможно - исправление в небольшом блоке кода с кнопкой Copy. Исправление написано именно для этого сервера: если в нём сказано $SERVER_PORT, то эта переменная действительно задана при запуске. Кнопка Re-run запускает проверку заново по требованию.
Что именно проверяется, зависит от рантайма сервера. Серверы Node.js, Bun, Deno, Go и Python получают полный набор: команда запуска, порт и хост, сборка, версия. Веб-хостинг получает проверки индексного файла и сборки. Другие типы серверов, такие как Java, .NET или Rust, выполняют собственную сборку и команду запуска, поэтому для них проверяются только секреты, и карточка прямо об этом говорит, а не молчит.
Нечего запускать: скрипты запуска и входные файлы#
Самая частая проблема - приложение, в котором серверу нечего запускать. Контейнер стартует, не находит команды и завершается за секунду. В консоли короткая вспышка вывода, а потом ничего, и это сбивает с толку, если вы этого не ждёте.
Для сервера Node.js запуск выполняет npm install, а затем npm run start. Если в package.json нет скрипта start, берётся первый найденный входной файл из фиксированного списка: index.js, server.js, app.js, main.js, bot.js, index.mjs, index.cjs, те же имена в src/, а затем TypeScript-варианты вроде index.ts и src/server.ts (запускаются через ts-node). Если нет ни того, ни другого, карточка показывает "No start script and no entry file" с уровнем fail.
Вызывающий это package.json обычно выглядит так - проект, который всегда запускали только через npm run dev на ноутбуке:
{ "name": "my-api", "main": "src/api.js", "scripts": { "dev": "nodemon src/api.js" }, "dependencies": { "express": "^4.19.2" }}src/api.js не входит в список запасных имён, а start нет. Исправление - одна строка:
{ "scripts": { "dev": "nodemon src/api.js", "start": "node src/api.js" }}package.json, который не разбирается, сообщается отдельно и раньше всего остального, потому что npm его отвергает и всё, что дальше, уже не важно. Карточка предлагает запустить npm install локально и прочитать ошибку. Обычно виновата лишняя запятая в конце.
У остальных рантаймов есть свои варианты той же проверки:
- Bun запускает
bun install, затемbun run start, если вpackage.jsonесть скрипт start, иначе входной файл, указанный на вкладке Startup, иначе первый файл из того же запасного списка. Если на вкладке Startup указан файл, которого нет в ветке, но запасной вариант есть, карточка сообщает, какой файл реально запустится, и советует поправить вкладку Startup. - Deno запускает
deno task startизdeno.jsonилиdeno.jsonc. Если задачи start нет, он запускает входной файл с вкладки Startup командойdeno run -A, которая выдаёт все разрешения, и карточка об этом предупреждает. Запасные имена:main.ts,mod.ts,server.ts,index.ts,main.js,src/main.tsиsrc/index.ts. Если естьpackage.json, но нетdeno.json, появляется заметка: на сервере Deno никто не запускаетnpm install, поэтому либо дайте Deno самому читать зависимости, либо используйте спецификаторыnpm:. - Python запускает входной файл, указанный на вкладке Startup. Если он не указан, панель ищет
main.py,bot.py,app.py,run.pyи__main__.pyв таком порядке и при подключении направляет вкладку Startup на первый найденный. Если на вкладке Startup указанserver.py, а такого файла в ветке нет, это fail, и в исправлении будет назван найденный кандидат. - Go собирает пакет, указанный на вкладке Startup (
GO_MAIN, по умолчанию.), командойgo build, и неудачная сборка останавливает запуск. Проверке нужныgo.mod, Go-файлы в этой папке,package mainиfunc main(). Папка с пакетом-библиотекой помечается как таковая, потому чтоgo buildдля библиотеки не создаёт программу.
Случай с Go ловит раскладку, на которой спотыкаются многие: в корне модуля лежит пакет-библиотека, а программа находится в cmd/server. Задайте GO_MAIN на вкладке Startup как ./cmd/server, и находка станет зелёной. Если go.mod нет вообще, выполните go mod init с именем вашего модуля, затем go mod tidy и закоммитьте и go.mod, и go.sum.
Неверный порт, неверный хост#
Вторая по частоте проблема - приложение, которое прекрасно запускается, но недоступно. Она самая раздражающая: консоль пишет "listening", а браузер молчит.
Снаружи доступен только собственный порт сервера. Панель при запуске задаёт две переменные окружения, PORT и SERVER_PORT, равные этому порту. Приложение с жёстко прописанным 3000, 5000, 8000 или 8080 слушает порт, на который ничего не маршрутизируется, если только ваш выделенный порт случайно не совпадает с этим числом. А приложение, привязанное к localhost или 127.0.0.1, доступно только изнутри собственного контейнера, какой бы порт оно ни использовало. Подробнее о том, почему так, читайте в статье порты игровых серверов: как это устроено - для приложений действуют те же правила.
Проверка читает сначала скрипт запуска, затем входной файл и сообщает:
- скрипт запуска, который фиксирует порт, например
next start -p 3000или--port 8080(warn); - входной файл, где вызывается
.listen(3000)или заданport = 5000(warn); - входной файл, который привязывается к
localhostили127.0.0.1(warn); - входной файл, который читает
process.env.PORT,SERVER_PORT,Bun.env.PORTилиDeno.env.get("PORT")(pass); - веб-сервер не найден вовсе (info) - для бота Discord или Telegram это нормально, а если вы ждали HTTP, стоит присмотреться.
Этот входной файл на Express вызывает сразу две находки: жёсткий порт и привязку к localhost:
const express = require('express');const app = express();app.get('/', (req, res) => res.send('ok'));app.listen(3000, 'localhost', () => console.log('listening on 3000'));Исправление, которое предлагает карточка:
const port = Number(process.env.PORT);app.listen(port, '0.0.0.0', () => console.log(`listening on ${port}`));Та же идея в других рантаймах. Python: читайте порт через int(os.environ.get("SERVER_PORT", os.environ.get("PORT", 8080))) и передавайте host="0.0.0.0". Go: http.ListenAndServe(":"+os.Getenv("PORT"), mux) - голое двоеточие слушает на всех интерфейсах, а строка "localhost:8080" попадёт в находки. Bun: Bun.serve({ port: Number(process.env.PORT), hostname: '0.0.0.0', fetch }).
У Deno есть особый случай, о котором стоит знать. Deno.serve(handler) без порта слушает 8000, а это не ваш выделенный порт. Универсальная проверка лишь сказала бы, что не смогла определить используемый порт, а эта называет проблему прямо:
Deno.serve( { port: Number(Deno.env.get("PORT")), hostname: "0.0.0.0" }, (_req) => new Response("ok"),);Иногда карточка пишет "Could not tell which port" с уровнем info. Так бывает, когда сервер создаётся там, куда проверка не заглядывает, например в файле, импортируемом из входного. Это не ошибка: откройте файл, где вызывается listen, и проверьте глазами.
Сборка, которую никто не запускал#
У фреймворков с этапом сборки ошибка особого рода: команда запуска ищет результат сборки, которого нет. Проверка знает четыре таких фреймворка и то, что каждый из них создаёт: Next.js собирает .next, Nuxt - .output, Angular и Vite - dist. Если проект зависит от одного из них, проверка задаёт три вопроса: запускает ли сборку скрипт start, prestart или postinstall; если нет, закоммичен ли результат сборки; а если нет и того, это fail.
Типичный package.json для Next.js, который не пройдёт проверку:
{ "scripts": { "dev": "next dev", "build": "next build", "start": "next start" }}Скрипт build есть, но на сервере его никто не вызывает. Исправление от карточки сначала собирает, а затем запускает на нужном хосте и порту:
{ "scripts": { "build": "next build", "start": "next build && next start -H 0.0.0.0 -p $SERVER_PORT" }}Альтернативы - postinstall, который запускает сборку, или коммит самого результата сборки. Коммит .next работает, и карточка его принимает, напоминая, что пересобирать придётся перед каждым push. Большинство забывает об этом за неделю. Мы предпочитаем собирать на сервере.
Скрипт запуска, поднимающий сервер разработки, - это отдельное предупреждение. next dev, nuxt dev, ng serve и голый vite медленные, без минификации и часто привязаны к localhost. Для Vite предлагается vite build && vite preview --host 0.0.0.0 --port $SERVER_PORT, для Nuxt - nuxt build && node .output/server/index.mjs. О том, как сборка влияет на память небольшого сервера, рассказывает руководство по деплою Next.js.
Обычный TypeScript обрабатывается так же, без фреймворка. Если typescript есть в зависимостях, а скрипт запуска выполняет node dist/index.js, но dist/index.js не закоммичен и перед запуском никто не вызывает tsc, карточка сообщает "TypeScript is never compiled" и предлагает tsc && node dist/index.js.
Несовпадение версий рантайма#
Проект, написанный под одну мажорную версию рантайма, может не установиться или не запуститься на другой, обычно из-за нативного модуля или смены синтаксиса. Проверка сравнивает то, что просит репозиторий, с тем, что запущено на сервере.
Для Node.js она сначала читает .nvmrc, затем engines.node в package.json. Если в .nvmrc стоит 20, а сервер работает на Node 22, это предупреждение. С engines диапазон >=18 проходит на любой версии от 18 и выше, а ^20 или просто 20 - только на Node 20. Предлагаемое исправление - либо выбрать подходящую версию в поле Runtime version на той же вкладке, либо обновить файл. Выбирайте по тому, что из этого правда: если проекту действительно нужен Node 20, меняйте сервер; если .nvmrc остался со времён двухлетней давности, меняйте файл.
22Для Go читается строка go в go.mod. Если go.mod требует более новый Go, чем на сервере, сборка либо перед каждым запуском скачивает новый toolchain, а это каждый раз требует сети и места на диске, либо отказывается собираться. Выберите эту версию Go или новее в Runtime version, либо понизьте строку go, если коду на самом деле не нужна новая версия.
Проверка версии выполняется, только если образ сервера называет версию, которую проверка может прочитать. Если определить не удаётся, она молчит, а не гадает.
Python, веб-хостинг и другие особенности рантаймов#
У Python есть две находки, которые выручают тех, кто пришёл с других платформ.
Сервер устанавливает requirements.txt через pip перед запуском. Проект, у которого зависимости описаны только в pyproject.toml, получает предупреждение, потому что ничего из этого не будет установлено. Экспортируйте файл requirements и закоммитьте его:
$ pip freeze > requirements.txt$ uv pip compile pyproject.toml -o requirements.txtПодойдёт любая из команд; вторая оставляет в файле только то, что объявлено в pyproject.toml, а не всё, что стоит в вашем локальном окружении. Подробнее о фиксации версий - в руководстве по requirements в Python.
Procfile помечается как непрочитанный. Сервер запускает входной файл с вкладки Startup, а не команду из Procfile. Если в вашем Procfile написано web: gunicorn app:app, эта команда игнорируется, поэтому убедитесь, что входной файл сам запускает сервер.
Веб-хостинг проще и строже. Хост отдаёт папку как есть и никогда не запускает npm run build. Проверке нужен index.html, index.php или index.htm в корне. Если ничего из этого нет, но есть dist/index.html, build/index.html, out/index.html, public/index.html или _site/index.html, она сообщает об этом и предлагает закоммитить собранные файлы или направить путь деплоя на эту папку. Проект со скриптом build, но без закоммиченного результата сборки получает отдельное предупреждение по той же причине. О сборке см. руководство по хостингу статических сайтов.
Секреты: закоммиченные .env и .env.example#
Это единственная находка, которая касается безопасности, а не того, запустится ли приложение, и она выполняется для любого типа сервера.
Любой файл с именем .env, .env.local, .env.production, .env.prod, .env.development или .env.dev в любом месте дерева - это fail: "A .env is committed to the repository". Каждый пароль и ключ в нём доступен любому, кто может читать репозиторий, в каждом коммите с момента добавления. На веб-хостинге хуже: файл отдаётся вместе с остальной папкой, и его можно скачать прямо с сайта.
Исправление по порядку:
$ git rm --cached .env$ echo ".env" >> .gitignore$ git commit -m "Stop tracking .env"$ git pushЗатем замените учётные данные - пароли баз данных, токены API, токены ботов - и создайте новый .env на сервере через вкладку Files. .env никогда не скачивается с GitHub, так что на сервере есть только тот файл, который вы положили туда сами. Более широкую практику описывает статья переменные окружения и секреты.
Для .env.example появляется находка уровня info со списком объявленных в нём ключей, до тридцати, чтобы с первого взгляда было видно, что нужно серверному .env. Значения из примера не показываются. Если проект использует dotenv или python-dotenv, а .env.example нет, карточка отмечает, что приложение читает .env, и напоминает его загрузить.
Чего pre-flight check поймать не может#
Проверка читает файлы. Она ничего не запускает, и этим определяются её пределы.
- Ошибки времени выполнения. Отсутствующая переменная окружения, из-за которой приложение падает на первом запросе, база данных, отказывающая в соединении, опечатка в маршруте - ничего из этого не видно в списке файлов.
- Память. Сборка Next.js, которой нужно больше RAM, чем есть в тарифе, пройдёт проверку и упадёт при сборке. Как это распознать, объясняют лимиты памяти Node.
- Непрямые порты. Порт, прочитанный из конфигурационного файла или заданный в импортируемом модуле, может отображаться как "Could not tell". Проверьте его вручную.
- Приватные зависимости. Пакету из приватного реестра при установке нужны учётные данные; проверка установку не тестирует.
- Всё, что вне ветки. Загруженные файлы, собственный
.envсервера и содержимое баз данных не входят в репозиторий.
Зелёная карточка означает, что при запуске найдётся что исполнять, на нужном порту, собранное и на подходящей версии. Это убирает скучные отказы, так что то, что остаётся в консоли, - настоящая ошибка.
Когда консоль всё же показывает падение, панель "Work on this server with AI" на вкладке Console содержит пункт "Server won't start", который копирует готовый запрос с хвостом консоли. Находки pre-flight попадают и в контекст ИИ, так что ассистент, которого просят починить репозиторий, начинает с того, что уже известно как неисправное. Подключение и деплой по push описаны в руководстве по деплою с GitHub, а переключатели - в разделе о деплое в наших руководствах.
FAQ#
Блокирует ли pre-flight check деплой?
Нет. Он сообщает, но не останавливает. Вы можете подключить репозиторий с находками уровня fail, и деплой пойдёт и упадёт так, как предсказала карточка. Смысл в том, чтобы увидеть это до, а не после.
Почему карточка пишет, что файла нет, хотя он есть в репозитории?
Обычно три причины: файл лежит в другой ветке, не в подключённой; путь деплоя указывает на подпапку, где его нет; или репозиторий настолько велик, что GitHub вернул неполный список файлов, о чём карточка предупреждает вверху. Сначала проверьте название ветки в строке заголовка карточки.
Я исправил проблему и сделал push. Почему карточка всё ещё её показывает?
Долго так быть не должно. Голова ветки проверяется при каждом открытии, поэтому новый коммит вызывает свежее чтение. Если проблема всё ещё показана, нажмите Re-run и убедитесь, что вы отправили push в ту ветку, с которой связан сервер, а не в другую.
Может ли проверка увидеть значения в моём .env?
Нет. О закоммиченном .env сообщается по имени, а его содержимое никогда не запрашивается. .env.example читается, но на карточке появляются только имена ключей. Любая строка на карточке перед показом также проходит через редактор секретов.
Какие рантаймы охватывает проверка?
Node.js, Bun, Deno, Go и Python получают проверки запуска, порта, сборки и версии; веб-хостинг получает проверки индексного файла и сборки. Остальные типы серверов получают проверки закоммиченных секретов и заметку о том, что проверки рантайма к ним не применяются.
Работает ли это с GitLab или Bitbucket?
Нет. Git-деплой на RE:NODE работает только с GitHub, через GitHub App, и проверка читает репозиторий с доступом этого App - поэтому приватные репозитории работают, если App их видит.


