Для AI-агентов: markdown этой страницы — /docs-content/infra/deploy/deploy.md индекс документации — /llms.txt
Полный деплой приложения
POST /v1/infra/servers/:id/deploy
Полный цикл деплоя за один запрос: установка рантайма → остановка предыдущего сервиса → скачивание архива → распаковка → установка зависимостей → запись .env → команды preStart → создание systemd-юнита → запуск → проверка работоспособности. Используйте вместо цепочки /upload + /exec — это быстрее. Если шаг runtime завершается ошибкой, деплой не выдаёт команду остановки, не очищает и не заменяет дерево приложения, не читает и не перезаписывает .env. Установщик рантайма работает с правами root и может изменить системные зависимости хоста, поэтому непрерывная работа прежнего приложения не гарантируется. После успешного runtime последующие ошибки сохраняют прежнее поведение: нового отката нет, юнит может остаться остановленным, а дерево приложения — уже заменённым. По умолчанию ответ — JSON (рекомендуется для AI-агентов): платформа отвечает 200 перед первым шагом выкладки, а тело дописывает по завершении всех шагов — всё это время соединение удерживается открытым. Спящая машина сначала просыпается, и до конца пробуждения клиент не получает ни статуса, ни заголовков, поэтому таймаут HTTP-клиента должен покрывать и пробуждение, и саму выкладку. Для построчного прогресса передайте ?stream=true — сервер вернёт поток SSE (Server-Sent Events) с событиями на каждом шаге.
Для AI-агентов и MCP-клиентов обязательно используйте JSON-режим (без ?stream=true). Официальный MCP-клиент Вайбкод (инструмент manage_server_deploy) снимает этот параметр сам и всегда работает в JSON-режиме.
Секреты приложения
Секреты задаёт владелец в карточке приложения. При деплое значение секрета перекрывает одноимённое поле env; имя без значения не перекрывает явно переданный env. PORT задаёт платформа. Ответ warnings перечисляет проигнорированные имена и секреты без значения со ссылкой на карточку, без самих значений.
На отдельной машине платформа записывает временный файл и затем заменяет им /var/lib/vibe/app-secrets/<служба>.env от root с правами 0600, вне каталога приложения. Каталог /var/lib/vibe/app-secrets принадлежит root и имеет права 0700. Пути extractTo и dataDirs не должны пересекаться с этим каталогом (включая его родительские и вложенные каталоги). Дополнение /etc/systemd/system/<служба>.service.d/50-vibe-secrets.conf подключает его через EnvironmentFile=-/var/lib/vibe/app-secrets/<служба>.env. Авторский .env не меняется. Удалённые имена исчезают из файла. Команды install и preStart секретов не получают.
При systemd: false файл и дополнение также записываются. Чтобы подключить их самостоятельно, используйте systemd-службу с тем же serviceName и выполните systemctl daemon-reload и перезапуск службы. Для другого менеджера процессов нужен загрузчик формата systemd EnvironmentFile; не выполняйте этот файл как shell-скрипт. Доступ к файлу требует root.
В Галактике секреты добавляются только в окружение запускаемого контейнера. При переносе, восстановлении и замене ключа они доставляются заново; ранее доставленные, а затем удалённые имена передаются пустыми, чтобы не вернуться из образа. При восстановлении в новое приложение набор зашифрованных секретов и история имён копируются в его карточку при совпадении владельца и портала.
Значения не возвращаются через API и не публикуются в журнале деплоя. Тот, кто выполняет команды на машине или код приложения, может прочитать окружение процесса. Не давайте этот доступ тем, кому нельзя читать секреты.
Многострочные значения храните в base64: закодируйте значение в одну строку перед сохранением секрета и декодируйте внутри приложения. Платформа не декодирует base64 автоматически.
Деплой и доступ приложения к порталу
Ключ, которым вызван POST /v1/infra/servers/:id/deploy, не переносится автоматически в окружение приложения. Успешная выкладка и проверка работоспособности не проверяют доступ приложения к данным портала.
При успешном деплое с OAuth-app-ключом ответ добавляет одно предупреждение об этой границе: в warnings JSON-ответа отдельной виртуальной машины и Galaxy-приложения, а в SSE — в warnings события done. Это напоминание о настройке авторизации, а не результат проверки учётных данных runtime: оно приходит независимо от переданного env, preserveEnv и выбранного рантайма, включая static. Успех деплоя и прежние предупреждения сохраняются. При личном ключе или ошибке деплоя это предупреждение не добавляется.
Для работы от имени посетителя передайте приложению ключ того же OAuth-приложения, которое выдало пользовательскую сессию. Сервер приложения читает входящий X-Vibe-Authorization и передаёт его значение в Authorization исходящего запроса к /v1/*, вместе с ключом приложения в X-Api-Key. Для headless-сервиса явно передайте личный ключ через env, ограничьте его права минимально необходимыми scopes и оставьте доступ к серверу в OWNER_ONLY. Подробности обеих схем — Контракт приложения.
BITRIX_API_KEY и BITRIX_API_BASE_URL, если их читает ваш код, — выбранные клиентом имена переменных. Платформа не создаёт их автоматически: передайте нужные значения явно и настройте их чтение в приложении. Отсутствие этих имён в env само по себе не доказывает отсутствие учётных данных у приложения.
Galaxy-приложение — деплой сразу после создания
Если сервер — galaxy-приложение (поле createdVia равно galaxy в ответе POST /v1/infra/servers), деплой запускают сразу после создания, не дожидаясь blackholeStatus: "CONNECTED" — контейнер собирается самим деплоем. Модель размещения, жизненный цикл и стоимость — на странице Galaxy-приложение.
Отличия деплоя для galaxy-приложения:
runtimeобязателен.startобязателен для всех рантаймов, кромеstatic; дляstaticполе необязательно и игнорируется. Безruntimeзапрос вернёт400 GALAXY_DEPLOY_RUNTIME_REQUIRED.- Источник — встроенный
source.content(архив в base64). Вариантыsource.urlиsource.versionIdдоступны там, где платформа включила для вас выкладку по ссылке. Иначе они отклоняются с400 GALAXY_DEPLOY_CONTENT_ONLY. Ссылку стоит предпочесть, когда она есть: архив по ней скачивает сам хост, поэтому в теле запроса ничего крупного не едет и потолок на встроенный архив не действует. Ссылка обязана вести в хранилище исходников — сохранённая версия и подписанная ссылка на неё подходят, сторонний адрес отклоняется с400 GALAXY_SOURCE_URL_NOT_ALLOWED. У отдельной виртуальной машины такого ограничения нет. Dockerfileв архиве не требуется — платформа сгенерирует его сама изruntimeиport, а для рантаймов кромеstaticтакже изstart. Static всегда получает платформенную команду nginx. Если положить свойDockerfileв архив, он будет проигнорирован (платформа всегда генерирует свой). См. «Известные особенности» ниже.- Рабочая директория контейнера —
/opt/app(та же, что иextractToу обычной виртуальной машины): код копируется туда, поэтому работают и относительная команда (start: "node server.js"), и абсолютный путь (start: "node /opt/app/server.js"). Путь/appтоже ведёт на/opt/app— это символьная ссылка для обратной совместимости. То есть один и тот жеstartподходит и для VM-, и для galaxy-деплоя. - Деплой идёт по id самого приложения (
kindравенGALAXY_APP). Тот же запрос с id несущей машины (kindравенGALAXY) отклоняется —400 GALAXY_HOST_NOT_A_DEPLOY_TARGET. - Поля отдельной виртуальной машины к galaxy-приложению не применяются:
preStart,systemd,serviceName,cleanDeploy,extractTo,hardening,preserveEnv,dataDirs,dataDirsRecursive. Запрос с ними проходит, значения не действуют. Команды, которые нужно выполнить до старта приложения, задавайте вinstall— он выполняется при сборке контейнера. ПолеhealthPathдействует и задаёт путь проверки готовности внутри контейнера. Пути вextractToиdataDirsпри этом всё равно проверяются: недопустимое значение вернёт400 INVALID_EXTRACT_TOили400 INVALID_DATA_DIRS, и тот же код придёт наdataDirsRecursiveбезdataDirs.
Жизненный цикл galaxy-приложения — на странице Galaxy-приложение. Сценарий «деплой одним запросом при создании» — на странице Создать сервер.
Параметры
| Параметр | В | Тип | Обяз. | По умолч. | Описание |
|---|---|---|---|---|---|
id |
path | string (UUID) | да | — | ID BLACKHOLE-сервера, status: running, blackholeStatus: CONNECTED |
stream |
query | string | нет | — | true — потоковый режим SSE. Для JSON-ответа параметр не передавайте: JSON возвращается по умолчанию |
Поля запроса (body — JSON)
| Поле | Тип | Обяз. | По умолч. | Описание |
|---|---|---|---|---|
source |
object | да | — | Источник кода — ровно один из трёх вариантов ниже (url, content или versionId) |
source.url |
string | да (или content / versionId) |
— | HTTPS-URL архива с кодом. До 500 МБ |
source.content |
string | да (или url / versionId) |
— | Base64-строка архива. До 96 МБ на тело запроса, то есть около 72 МБ самого архива: base64 тяжелее исходных байт примерно на треть. Тело сверх потолка отклоняется кодом 413 INLINE_SOURCE_TOO_LARGE. Архив крупнее сохраняйте версией и разворачивайте через source.versionId |
source.versionId |
string | да (или url / content) |
— | ID снапшота из депо приложения, например v3 — см. Хранилище исходников |
start |
string | кроме static |
— | Команда запуска приложения (пример: cd /opt/app && node server.js). Для runtime: "static" поле необязательно и игнорируется: процессом управляет платформа |
port |
number | нет | 3000 |
Порт, на котором приложение слушает. Всегда 3000 для BLACKHOLE — если не передан, подставляется 3000. (В multipart-режиме порт указывать обязательно, см. таблицу ниже.) Платформа сама сообщает этот номер приложению в переменной окружения PORT — см. «Платформа сообщает порт в PORT» ниже. |
extractTo |
string | нет | /opt/app |
Директория распаковки. Проверяется до начала деплоя — недопустимый путь отклоняется с 400 INVALID_EXTRACT_TO (см. «Ошибки»). Managed standalone static (systemd: true) принимает только вложенный прикладной каталог под /opt или /srv; все пути под /var/lib и /opt/data отвергаются. При systemd: false и для остальных рантаймов действуют общие правила пути |
runtime |
string | нет | — | ID рантайма: node20, python311, php83, static и т. д. Список рантаймов и команда запуска для каждого — GET /v1/infra/runtimes. Сервер создаётся из стандартного образа Ubuntu 24.04 — без runtime на нём нет ни node, ни python3.11, ни php8.3, и start упадёт с command not found. Рантайм ставит версионированную команду: после python311 на отдельной виртуальной машине доступен python3.11, а команды python там нет. Альтернатива — поставить тулчейн вручную через preStart (см. ниже). |
install |
string | нет | — | Команда установки зависимостей (до 5000 символов) |
preStart |
string | нет | — | Команда до запуска сервиса: миграции БД, seed-скрипты, сборка, ручная установка системных пакетов (например, apt-get update && apt-get install -y nodejs npm если не передан runtime). До 5000 символов. Работает только для отдельной виртуальной машины (kind: "STANDALONE") |
env |
object | нет | — | Переменные окружения { "KEY": "value" } — будут записаны в .env и загружены systemd. Ключ PORT — зарезервирован платформой: она подставляет в него значение поля port, а присланное вами значение перекрывает и сообщает об этом в warnings ответа. Значение каждой переменной должно быть однострочным: перевод строки или нулевой байт в значении отклоняется с 400 VALIDATION_ERROR, а путь в сообщении указывает нарушивший ключ — env.<КЛЮЧ>. Многострочное значение, например ключ в формате PEM, передавайте одной строкой, заменив переносы двумя символами \n, и восстанавливайте их на стороне приложения |
displayName |
string | нет | — | Понятное имя приложения (2–100 символов) для карточки каталога Битрикс24. Указывайте всегда — иначе в каталог попадёт технический идентификатор, а ответ вернёт предупреждение |
description |
string | нет | — | Короткое описание, что делает приложение, для карточки каталога Битрикс24 (до 500 символов) |
changelog |
string | нет | — | Что изменилось в этой версии (обычный текст, до 2000 символов). При выходе новой версии текст публикуется подписчикам в ленту канала приложения в мессенджере Битрикс24. Заполняйте одной-двумя строками «что нового». Если не передать — в ленту уйдёт только номер версии («Обновление до vN»). Ключ, который не меняет данные Битрикс24, анонс в ленту не публикует: заметка версии сохраняется, карточка и иконка публикуются, в warnings приходит предупреждение |
baseVersionId |
string | зависит | — | Версия исходников, на которой основана эта выкладка, например "v12" — та, которую вы скачали перед правкой. Если в хранилище исходников сервера появилась версия новее, деплой отклоняется с 409 SOURCE_VERSION_STALE: в теле придёт номер актуальной версии и ссылка на её скачивание. Обязательна, как только в хранилище есть хотя бы одна версия и либо последнюю версию сохранил другой разработчик, либо у сервера есть команда разработки: деплой без поля отвечает 409 BASE_VERSION_REQUIRED и называет актуальную версию. Для первой выкладки в пустое хранилище поле не требуется; без команды оно также остаётся необязательным, пока последняя версия ваша, но чужая последняя версия делает его обязательным и на сервере без команды. Значение должно называть версию, с которой вы действительно начали: номер, переписанный из текста ошибки, подтверждает слияние, которого не было. Номер впереди хранилища (версии с таким номером не существует) отклоняется с 400 BASE_VERSION_NOT_FOUND |
systemd |
boolean | нет | true |
Создавать systemd-юнит для автозапуска |
cleanDeploy |
boolean | нет | true |
Очистить extractTo перед распаковкой. Установите false для merge-деплоя |
preserveEnv |
boolean | нет | false |
Сохранить существующий .env при очистке: файл читается до удаления extractTo и записывается обратно на шаге env. Возврат сохранённой копии по мере возможности применяется только после создания снимка и перехода к очистке. Ошибка runtime происходит раньше: прежний .env остаётся на месте, не читается и не загружается обратно. Если в этом же деплое передан env, он побеждает, а сохранённая копия отбрасывается. Работает только для отдельной виртуальной машины (kind: "STANDALONE") |
serviceName |
string | нет | app |
Имя systemd-юнита. Только a-z, 0-9, -. Создаёт {serviceName}.service. При смене имени между деплоями — см. «Известные особенности» |
healthPath |
string | нет | / |
Путь для проверки работоспособности — например, /health, /api/status. Только безопасные символы. Не-корневой путь допустим, пока приложение отвечает и на /: после успешной проверки этого пути деплой опрашивает и корень — ответ 5xx там останавливает деплой ошибкой, ответ 4xx даёт шаг app_root со статусом warning. Если приложение отвечает ТОЛЬКО на подпуть — см. «Известные особенности» |
debug |
boolean | нет | false |
Добавить диагностический шаг debug_info — ls -la, sha256sum файлов, file -i для отладки шагов сборки |
hardening |
string | нет | auto |
Режим запуска приложения. auto — запустить под непривилегированной учётной записью и вернуть прежний режим автоматически, если приложение не пройдёт проверку работоспособности. off — сразу запустить с правами администратора. См. «Известные особенности» |
dataDirs |
array | нет | — | Каталоги состояния вне extractTo, в которые должно писать само приложение, — например ["/opt/data"]. На хардненном деплое платформа создаёт каждый и передаёт сам каталог учётной записи приложения. До восьми путей, каждый — абсолютный нормализованный путь внутри /opt, /srv или /var/lib с собственным именем каталога, иначе 400 INVALID_DATA_DIRS. См. «Известные особенности» |
dataDirsRecursive |
boolean | нет | false |
Передать и содержимое каталогов из dataDirs, а не только сами каталоги. Нужно лишь для заранее развёрнутого дерева, которое приложение обязано переписывать. Для /opt/data не принимается. См. «Известные особенности» |
Поля запроса (body — multipart/form-data)
Альтернатива JSON — загрузка архива без base64. Полезно из скриптов bash/PowerShell.
| Поле формы | Тип | Обяз. | Описание |
|---|---|---|---|
file |
файл | да | Архив (tar.gz / zip). Потолок — 500 МБ, пока архив уходит в хранилище исходников потоком, и около 72 МБ, когда потоковая запись недоступна и архив читается в память: тогда отказ приходит кодом 413 INLINE_SOURCE_TOO_LARGE. Три условия потоковой записи — в «Известных особенностях». Архив принимается и под именем поля archive |
start |
string | кроме static |
Команда запуска. Для runtime: "static" поле необязательно и игнорируется |
port |
string | да | Порт (строкой: "3000") |
extractTo |
string | нет | По умолчанию /opt/app. Та же проверка, что и в JSON-варианте: managed standalone static принимает только вложенный прикладной каталог под /opt или /srv и отвергает /var/lib, а при systemd: false и для остальных рантаймов действуют общие правила; недопустимый путь отклоняется с 400 INVALID_EXTRACT_TO |
runtime |
string | нет | ID рантайма |
install |
string | нет | Команда установки |
displayName |
string | нет | Понятное имя приложения для каталога Битрикс24 |
description |
string | нет | Короткое описание для каталога Битрикс24 |
changelog |
string | нет | Что нового в этой версии (до 2000 символов) — публикуется в ленту канала приложения в мессенджере Битрикс24. Ключ, который не меняет данные Битрикс24, анонс не публикует: заметка версии сохраняется, в warnings приходит предупреждение |
baseVersionId |
string | зависит | То же поле, что в JSON-режиме: версия исходников, на которой основана выкладка ("v12"). Более новая версия в хранилище отклоняется с 409 SOURCE_VERSION_STALE. Когда хранилище непустое, пропущенное поле отклоняется с 409 BASE_VERSION_REQUIRED, если последнюю версию сохранил другой разработчик или у сервера есть команда разработки; номер впереди хранилища отклоняется с 400 BASE_VERSION_NOT_FOUND |
preStart |
string | нет | Команда до запуска. Работает только для отдельной виртуальной машины (kind: "STANDALONE") |
env |
string | нет | JSON-строка: {"KEY":"value"}. Это текстовое поле формы — если прислать env файлом или Blob-частью, запрос вернёт 400 VALIDATION_ERROR. Значения переменных — однострочные, как и в JSON-режиме |
systemd |
string | нет | "true" / "false" |
cleanDeploy |
string | нет | При multipart по умолчанию "false" (merge). Укажите "true" для чистого деплоя |
preserveEnv |
string | нет | "true" — сохранить существующий .env при очистке и записать его обратно на шаге env. Возврат по мере возможности применяется только после создания снимка и перехода к очистке. Ошибка runtime происходит раньше: прежний файл не читается и остаётся на месте без обратной загрузки |
serviceName |
string | нет | Имя systemd-юнита |
healthPath |
string | нет | Путь для проверки работоспособности |
debug |
string | нет | "true" — добавить диагностический шаг debug_info |
hardening |
string | нет | "auto" / "off" — режим запуска приложения. См. «Известные особенности» |
dataDirs |
string | нет | JSON-массив строкой: ["/opt/data"]. То же поле и та же проверка, что в JSON-варианте. Не-массив отклоняется с 400 VALIDATION_ERROR, недопустимый путь — с 400 INVALID_DATA_DIRS |
dataDirsRecursive |
string | нет | "true" / "false". Читаются только эти два литерала, опечатка не включает режим |
Шаги деплоя
Шаги выполняются последовательно. Если какой-то провалился — деплой останавливается.
| Шаг | Условие | Что делает | Таймаут |
|---|---|---|---|
runtime |
runtime передан |
Устанавливает рантайм до stop_existing. При повторном деплое с тем же — переустанавливает. Если установка завершается ошибкой, снимок .env, остановка и очистка ещё не начинались. Если установка прошла, но установщик сообщил о себе — например, один из источников пакетов отложен в сторону, чтобы apt оставался читаемым, или рантайм поставлен из запасного источника, — шаг возвращает статус warning, текст сообщений лежит в stdout, а деплой продолжается и завершается успешно. Такие состояния обычно разбираются сами на следующем деплое |
300с |
stop_existing |
всегда | systemctl stop {serviceName} — останавливает сервис с текущим именем serviceName и освобождает порт от оставшихся процессов этого же деплоя. Если порт публикует запущенный контейнер Docker Compose самого приложения — проекта, чья папка в метке Compose com.docker.compose.project.working_dir совпадает с extractTo или лежит внутри него, — шаг останавливает этот контейнер командой docker stop и называет его в stdout. Поднять контейнер снова должна команда start. Другие посторонние процессы не завершаются: шаг возвращает статус warning, деплой продолжается. Столько же — если остановку не удалось подтвердить: результат не пришёл за отведённое время ЛИБО сервер ответил, что остановить не смог. В обоих случаях шаг возвращает warning с текстом «исход неизвестен» и деплой идёт дальше — этот шаг деплой не прерывает |
105с |
clean |
cleanDeploy: true |
Удаляет всё в extractTo. Если сервер не выполнил команду (например, занят другой), шаг возвращает error и называет причину — деплой останавливается здесь, а не позже на download с непонятным сообщением |
30с |
download |
всегда | Получает и распаковывает архив. На отдельной VM в rollout надёжной загрузки платформа скачивает source.url сама и передаёт через туннель. Временные сетевые отказы получают до 4 попыток с backoff в общем бюджете 5 минут. До включения сохраняется прежняя загрузка с VM. Постоянные ошибки URL-политики, HTTP, размера, целостности и распаковки не повторяются только в новом режиме. source.content сохраняет прежние 2 попытки |
До 5 мин на получение source.url в новом режиме. Распаковка имеет отдельный бюджет |
cleanup_metadata |
после extract | Удаляет macOS-сайдкары (._*, .DS_Store) — молча если архив чистый. Здесь же удаляются небезопасные символические ссылки. Если разбор ссылок не успел пройти дерево целиком, шаг получает статус warning с пояснением в stdout, а выкладка продолжается |
~120с |
normalize_windows_paths |
после extract | Приводит пути с обратной косой чертой \ из zip, собранного PowerShell Compress-Archive, к обычному виду. Несёт тот же разбор символических ссылок и тот же статус warning, если он не успел пройти дерево целиком |
~120с |
debug_info |
debug: true |
Диагностика: ls -la, sha256sum файлов, file -i, локаль. Читает до 32 КБ в stdout |
— |
install |
install передан |
Запускает команду установки зависимостей | 300с |
env |
env передан либо preserveEnv: true сохранил прежний файл |
Пишет .env в extractTo с правами 0600. При восстановлении сохранённой копии шаг возвращает пояснение в stdout. Если после создания снимка и перехода к очистке деплой оборвался до этого шага, копия возвращается на диск без отдельного шага в ответе. При ошибке runtime снимка ещё нет, прежний файл не читается, обратная загрузка не выполняется, а файл остаётся на месте |
— |
platform_env |
всегда | Пишет платформенный файл .vibe-platform.env в extractTo с правами 0600 — в нём одна переменная PORT. Ваш .env этот шаг не читает и не перезаписывает. Если файл записать не удалось, шаг завершается ошибкой и деплой прерывается: без него приложение запустилось бы, не зная своего порта. Исключение — systemd: false: юнит там ваш, платформенный файл всё равно ничего не загружает, поэтому шаг возвращает warning и деплой продолжается |
— |
pre_start |
preStart передан |
Запускает команду до старта сервиса. Для миграций БД, seed-скриптов, сборки | 300с |
service_user |
systemd: true, hardening не равен off, а extractTo — вложенный каталог /opt, /srv или /var/lib, записанный без завершающей косой черты и без сегментов . и ... По любому другому пути хардненинг не применяется — шаг возвращает статус warning, а приложение продолжает работать с правами администратора |
Создаёт непривилегированную учётную запись vibeapp и передаёт ей владение каталогом extractTo, чтобы приложение работало без прав администратора. Если учётную запись создать или передать владение не удалось — статус warning, деплой продолжается с прежними правами |
120с |
data_dirs |
В теле деплоя объявлено поле dataDirs |
Создаёт объявленные каталоги и передаёт их учётной записи приложения, а в stdout перечисляет каждый путь и режим передачи. ok — переданы все объявленные каталоги. warning — какой-то не удалось создать или передать, либо деплой оказался не хардненным и передавать было нечего. Деплой при этом продолжается и хардненинг не снимается. Шага нет вовсе, если поле не передавали |
30с на каталог (120с в рекурсивном режиме) |
systemd |
systemd: true |
Создаёт {serviceName}.service и перечитывает конфигурацию systemd (systemctl daemon-reload) |
30с |
start |
всегда | systemctl enable --now {serviceName} |
30с |
hardening |
хардненный запуск откачен | Эмитится после start и оценки healthcheck (порядок в data.steps[] — start → hardening → healthcheck), потому что предупреждение об откате возможно только после неудачного старта под vibeapp. Сообщает, что приложение вернулось к работе с правами администратора. Приходит и при УСПЕШНОМ деплое, и при ПРОВАЛЬНОМ — всегда перед завершающим шагом healthcheck. Несёт машинное поле hardeningRollback (см. таблицу полей ответа). Статус всегда warning. В stdout — причина отката и что сделать для закрепления хардненного режима. Хвост журнала сервиса добавляется только на успешном деплое |
— |
healthcheck |
всегда | Проверка работоспособности — до 10 попыток curl localhost:{port}{healthPath} с интервалом 3 секунды, засчитывается ответ со статусом 200–399 от сервиса serviceName. Ответ 200–399 от постороннего процесса, удерживающего порт, шаг не засчитывает и завершается ошибкой. Важно: статус этого шага значит разное на разных видах серверов: на отдельной виртуальной машине (kind: "STANDALONE") ответ 4xx/5xx даёт error и деплой падает, у galaxy-приложения (kind: "GALAXY_APP") — warning, и деплой считается прошедшим |
~30с |
app_root |
healthPath отличается от / |
Опрашивает корень приложения — адрес, который открывает Битрикс24. ok — корень ответил 200–399. warning — ответил 4xx либо проверка не дошла: деплой продолжается, пояснение в stdout. Ответ 5xx корнем деплой не продолжает: он завершается ошибкой на шаге healthcheck, и шага app_root в ответе нет. При healthPath по умолчанию (/) шага нет вовсе — корень уже проверен |
~10с |
tunnel_routing |
всегда | Проверяет, что туннель Gateway маршрутизирует на нужный порт. У обычного сервера — командой агенту плюс опрос, у сервера с закреплённым портом — перезаписью настроек агента (см. port_pin). Статус warning (не error) — проверка не прошла, деплой не прерывается |
~10с |
port_pin |
сервер закреплён либо закрепление включено для портала | Записывает порт в настройки агента на машине, чтобы он пережил пробуждение и починку туннеля. ok — порт в настройках, warning — записать не удалось, приложение при этом работает, но после пробуждения агент снова выберет порт сам. Подробности — Изменить порт |
~20с |
Примеры
Примеры с внешним source.url содержат заголовок X-Skip-Source-Snapshot: при включённом хранилище исходников деплой со стороннего адреса без него возвращает 409 SNAPSHOT_REQUIRED. Подробности и альтернатива через хранилище — в разделе «Известные особенности».
curl — личный ключ
# JSON, Node.js с миграциями
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/deploy" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Skip-Source-Snapshot: deploy from external URL" \
-d '{
"source": { "url": "https://github.com/user/app/archive/main.tar.gz" },
"runtime": "node20",
"install": "cd /opt/app && npm install --production",
"preStart": "cd /opt/app && npx prisma migrate deploy",
"start": "cd /opt/app && node server.js",
"port": 3000,
"env": { "NODE_ENV": "production", "DATABASE_URL": "postgresql://localhost/mydb" }
}'
# Multipart — локальный архив
tar -czf app.tar.gz -C ./my-app .
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/deploy" \
-H "X-Api-Key: YOUR_API_KEY" \
-F "file=@app.tar.gz" \
-F "runtime=node20" \
-F "install=cd /opt/app && npm install --production" \
-F "start=cd /opt/app && node server.js" \
-F "port=3000"
curl — inline-архив (минимальный набор полей)
# Канонический inline-деплой: достаточно source.content + start.
# Тип архива определяется автоматически по сигнатуре первых байт —
# tar.gz, zip и tar.bz2 поддерживаются, отдельное поле формата не нужно.
B64=$(base64 < app.tar.gz | tr -d '\n')
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/deploy" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"source\": { \"content\": \"$B64\" },
\"start\": \"cd /opt/app && node server.js\",
\"port\": 3000
}"
Обязательны только два поля: source (один из content / url / versionId) и start. Архив можно завернуть и в zip (zip -r app.zip .), и в tar.gz (tar -czf app.tar.gz -C ./my-app .) — платформа распознаёт оба по содержимому.
curl — произвольный стек без рантайма
# Деплой без runtime — любое ПО ставится через preStart
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/deploy" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Skip-Source-Snapshot: deploy from external URL" \
-d '{
"source": { "url": "https://example.com/my-app.tar.gz" },
"preStart": "apt-get update -qq && apt-get install -y ffmpeg imagemagick",
"install": "cd /opt/app && npm install --production",
"start": "cd /opt/app && node server.js",
"port": 3000
}'
curl — OAuth-приложение
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/deploy" \
-H "X-Api-Key: YOUR_APP_KEY" \
-H "Authorization: Bearer USER_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Skip-Source-Snapshot: deploy from external URL" \
-d '{
"source": { "url": "https://github.com/user/fastapi/archive/main.tar.gz" },
"runtime": "python311",
"install": "cd /opt/app && pip install -r requirements.txt",
"start": "cd /opt/app && python3.11 -m uvicorn main:app --host 0.0.0.0 --port 3000",
"port": 3000
}'
JavaScript — личный ключ
const res = await fetch(
`https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/deploy`,
{
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
'X-Skip-Source-Snapshot': 'deploy from external URL',
},
body: JSON.stringify({
source: { url: 'https://github.com/user/app/archive/main.tar.gz' },
runtime: 'node20',
install: 'cd /opt/app && npm install --production',
start: 'cd /opt/app && node server.js',
port: 3000,
}),
}
)
const body = await res.json()
if (body.success) {
console.log(`Приложение живёт: ${body.data.appUrl}`)
} else {
console.error(`Упал на шаге ${body.error.step}: ${body.error.message}`)
}
JavaScript — OAuth-приложение
await fetch(
`https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/deploy`,
{
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_APP_KEY',
'Authorization': 'Bearer USER_SESSION_TOKEN',
'Content-Type': 'application/json',
'X-Skip-Source-Snapshot': 'deploy from external URL',
},
body: JSON.stringify({
source: { url: 'https://github.com/user/app/archive/main.tar.gz' },
runtime: 'node20',
install: 'cd /opt/app && npm install --production',
start: 'cd /opt/app && node server.js',
port: 3000,
}),
}
)
Поля ответа
| Поле | Тип | Описание |
|---|---|---|
success |
boolean | true — все шаги выполнены, приложение отвечает на проверку работоспособности |
data.steps |
array | Массив шагов процесса деплоя, в порядке выполнения |
data.steps[].step |
string | Имя шага: runtime, stop_existing, download, install, env, platform_env, pre_start, service_user, data_dirs, systemd, start, healthcheck, app_root, hardening, tunnel_routing, port_pin, и т. д. У galaxy-приложения (kind: "GALAXY_APP") имён два — build и healthcheck |
data.steps[].status |
string | ok — успешно, warning — шаг не сделал задуманное, но деплой продолжается (объяснение в stdout или stderr — так сообщают о себе stop_existing, normalize_windows_paths, cleanup_metadata, service_user, start, hardening, app_root, tunnel_routing), error — провал (только последний шаг может иметь этот статус). У шага runtime warning означает другое: установка ПРОШЛА, но установщик рантайма сообщил о себе — читайте stdout |
data.steps[].duration |
number | Длительность шага в секундах. Присутствует не у всех шагов — только у тех, которые сами отмеряют время выполнения (runtime, install, pre_start, service_user, normalize_windows_paths, cleanup_metadata, debug_info) |
data.steps[].stderr |
string | Стандартный поток ошибок (stderr) шага при status: "error", а также при status: "warning" у шага start — там он несёт исходную причину, по которой сервис не запустился с первой попытки |
data.steps[].stdout |
string | Стандартный вывод (stdout), заполняется для шага debug_info (до 32 КБ), а также несёт объяснение у шагов со статусом warning — в том числе runtime, service_user и hardening |
data.steps[].causeCode |
string | Только для ошибки download: стабильная машинная причина. Возможные значения: DOWNLOAD_RELAY_BUSY, DOWNLOAD_URL_DENIED, DOWNLOAD_SOURCE_UNREACHABLE, DOWNLOAD_HTTP_STATUS, DOWNLOAD_TOO_LARGE, DOWNLOAD_INTEGRITY_MISMATCH, DOWNLOAD_RELAY_INTERRUPTED, DOWNLOAD_COMMIT_UNCONFIRMED, DOWNLOAD_OUTCOME_UNCONFIRMED, DOWNLOAD_EXTRACT_FAILED |
data.steps[].retryable |
boolean | Только вместе с causeCode: можно ли безопасно повторить тот же запрос. Для DOWNLOAD_COMMIT_UNCONFIRMED и DOWNLOAD_OUTCOME_UNCONFIRMED всегда false: сначала прочитайте список операций, не повторяйте deploy вслепую. Не выводите значение из текста message |
data.steps[].attempts |
number | Число реально начатых попыток получить source.url: 0 — отказ до скачивания, 1…4 — источник запрашивался. Поле отсутствует, если старый агент не вернул терминальный результат и число попыток доказать нельзя |
data.steps[].httpCode |
number | Только у шага healthcheck. HTTP-код ПОСЛЕДНЕЙ попытки проверки, а у galaxy-приложения — код ответа приложения на проверку. Поле отсутствует, если ни одна попытка кода не получила (соединение не установилось) — отсутствие и значит «кода не было», ноль или null вместо него не приходят. У galaxy-приложения поле теряется ещё в одном случае: деплой, доведённый до конца после обрыва связи с хостом, теряет код вместе с журналом сборки. Это факт о попытке, а НЕ вердикт: у шага со status: "error" поле законно может быть 200 |
data.steps[].healthPath |
string | Только у шага healthcheck. Путь, который фактически проверяли (уже с учётом значения по умолчанию /). У galaxy-приложения — путь, по которому платформа обращалась к приложению |
data.steps[].portOwner |
string | Только у шага healthcheck. Кому принадлежит порт по последней успешной попытке: service — вашему сервису (проверено), foreign — постороннему процессу, unknown — владелец не установлен. У unknown две причины: зонд владения не запускался, потому что ответ был вне диапазона 200–399, либо зонд запустился и не смог назвать владельца — так отвечает порт, который держит не сам юнит, а оставленный вашей командой start фоновый процесс. Значение НЕ означает «порт свободен» |
data.steps[].hardeningRollback |
string | Только у шага hardening. completed — откат завершён: юнит и владение каталогом extractTo вернулись к администратору, приложение работает с полными правами. incomplete — откат начался и не завершился: приложение МОЖЕТ по-прежнему работать под vibeapp. Поле описывает возврат ЮНИТА и владения extractTo, и только их |
data.serviceName |
string | Имя systemd-юнита (эхо параметра или "app") |
data.status |
string | "running" после успешного деплоя |
data.appUrl |
string | HTTPS-адрес приложения: https://{subdomain}.vibecode.bitrix24.tech |
data.sleepAfterMinutes |
number | null | Сохранённый таймаут авто-сна в минутах на момент начала деплоя. Только в успешном ответе отдельной виртуальной машины (kind: "STANDALONE"), независимо от наличия предупреждения или окон пробуждения. null — выбор «Никогда», но при действующем расписании фактическим становится платформенный таймаут после окна |
data.source |
object | Итог автосохранения исходников. Только при success: true. В SSE-режиме приходит отдельным событием event: source. Неудавшийся деплой блока не содержит, но версия в хранилище может уже существовать — см. «Известные особенности». Полный контракт — Хранилище исходников |
data.source.autoSaved |
boolean | true — снимок исходников сохранён или привязан к деплою |
data.source.savedVersionId |
string | Версия vN сохранённого снимка. Присутствует при autoSaved: true |
data.source.skippedReason |
string | null | Причина, по которой снимок не сохранён, либо null. Значения — Хранилище исходников |
data.operationId |
string | Идентификатор операции выкладки. Тот же, что в заголовке ответа X-Vibe-Operation-Id. По нему исход читается постфактум — Исход выкладки. На отдельной машине запись заводится до конвейера, но у спящей машины блокирующее пробуждение завершается раньше, чем id доезжает по этому соединению; у galaxy-приложения запись заводится после захвата mutex, а id приходит только с терминальным ответом. Отсутствует, если соединение оборвалось до доставки либо платформа не смогла завести запись |
warnings |
array<string> | Присутствует, только если есть предупреждения — например, displayName/description не переданы либо не применены (см. «Известные особенности») либо шаг runtime завершился со статусом warning — тогда строка указывает, что сообщения установщика рантайма лежат в stdout этого шага. Успешный деплой с OAuth-app-ключом также добавляет предупреждение о настройке доступа приложения к порталу (см. выше). В SSE-режиме приходит в том же event: done |
Заголовки ответа
На отдельной виртуальной машине (kind: "STANDALONE") выкладка заводит запись операции ДО конвейера. На уже доступной машине id доезжает до клиента до начала работы. На спящей машине запись уже видна в списке операций сервера, но блокирующее пробуждение завершается раньше, чем по этому соединению приходят HTTP-статус, заголовки или SSE-кадр:
X-Vibe-Operation-Id: clz9k2m4x0001qw8h3f7d2n5p
Тот же идентификатор дублируется в теле (data.operationId при успехе, error.operationId при отказе), а в режиме ?stream=true приходит ещё и первым кадром до первого event: step — после завершения возможного блокирующего пробуждения:
event: operation
data: {"operationId":"clz9k2m4x0001qw8h3f7d2n5p"}
Кадр нужен потому, что браузерный EventSource заголовков ответа не отдаёт вовсе.
У galaxy-приложения (kind: "GALAXY_APP") запись заводится после захвата mutex приложения, при входе в первый шаг. Идентификатор приходит в заголовке и в терминальном JSON-теле; раннего SSE-кадра у этой ветки нет. Если соединение оборвалось раньше ответа, найдите запуск через GET /v1/infra/servers/:id/operations.
Отсутствие заголовка НЕ означает, что выкладка не запускалась. Связь с отдельной спящей машиной могла оборваться во время пробуждения, связь с galaxy могла оборваться до терминального ответа, либо платформа могла не суметь завести запись и продолжить без идентификатора. Повторять выкладку на этом основании нельзя — второй запуск ляжет поверх приложения, которое уже могло подняться. Сначала прочитайте последние операции сервера, состояние и логи; адресный исход — Исход выкладки.
Заголовки приходят до конвейера, после возможного блокирующего пробуждения; тело — в конце. В режиме JSON на отдельной виртуальной машине (kind: "STANDALONE") платформа отдаёт 200 и заголовки перед первым шагом, а тело ответа дописывает, когда выкладка завершена: всё это время соединение удерживается открытым пробелами. Если машина спала, деплой сначала создаёт операцию, затем будит машину, и до конца пробуждения — до 6.5 минут — клиент не получает ни статуса, ни заголовков, ни самого идентификатора по этому соединению. Пока запрос молчит, операция уже видна как running через GET /v1/infra/servers/:id/operations, без шагов выкладки. Если пробуждение вернуло отказ и запись операции удалось создать, ответ несёт тот же id в X-Vibe-Operation-Id и error.operationId; 503 WAKE_TIMEOUT остаётся 503, а после успешной асинхронной записи код операции — WAKE_TIMEOUT. Сразу после ответа операция может кратко оставаться running; при сбое терминальной записи она позднее читается как unknown. Клиент с коротким таймаутом закрывает соединение раньше и видит ответ 200, тело которого пусто или не разбирается как JSON, а при обрыве во время пробуждения ответа нет вовсе. Выкладка при этом продолжается на сервере, и повторять её нельзя. Сколько платформа удерживает соединение и какой таймаут ставить — «Долгий деплой и таймауты HTTP-клиента» в разделе «Известные особенности». Пошаговый прогресс вместо ожидания даёт режим ?stream=true, а исход прерванной выкладки читается по идентификатору операции — Исход выкладки.
У galaxy-приложения (kind: "GALAXY_APP") ответ приходит целиком по завершении сборки — и заголовки, и тело, — поэтому запас по таймауту нужен такой же, а признака «работа идёт» до ответа нет.
При success: false есть дополнительно error.code, error.message, error.step с именем провалившегося шага, а data.steps содержит хронологию шагов до провала. Если сработал автооткат хардненного запуска, в конверте ошибки появляется ещё error.hardeningRollback — то же значение, что у шага hardening. Поле есть только у обычного ответа. При ?stream=true конверт с ним не приходит: исход читается по кадру event: step со status: "error" и по предшествующему ему кадру шага hardening. Кадр event: error в потоке всё же бывает — при обрыве операции и когда сервер занят другой операцией, — но hardeningRollback он не несёт. Отсутствие поля означает «откат не запускался», а НЕ «изоляция не применялась» — на второй вопрос отвечает шаг service_user (ok — применялась, warning — нет, шага нет вовсе — деплой не дошёл до этого места). На отдельной виртуальной машине (kind: "STANDALONE") такой ответ приходит с HTTP-статусом 200 — соединение удерживается с начала деплоя, поэтому провал шага виден по полю success, а не по статусу. У galaxy-приложения (kind: "GALAXY_APP") та же ошибка приходит со статусом 502.
Форма ответа galaxy-приложения отличается. У galaxy-приложения (kind: "GALAXY_APP") успешный ответ несёт data вида { "id", "status", "appUrl", "buildLog", "steps", "autoSaved", "skippedReason" }, плюс warnings[], когда displayName/description не переданы либо не применены, а также при деплое с OAuth-app-ключом (см. выше). Поле sleepAfterMinutes в эту форму не входит — оно относится только к ответу деплоя отдельной виртуальной машины. Поля autoSaved и skippedReason лежат прямо в data, а не в data.source, а поля data.source.savedVersionId в ответе нет — итог автосохранения исходников читайте по data.autoSaved. Подробности сборки остаются в поле buildLog.
Массив data.steps[] у galaxy-приложения короче, чем у отдельной машины, и состоит максимум из двух шагов:
"steps": [
{ "step": "build", "status": "ok" },
{ "step": "healthcheck", "status": "ok", "httpCode": 200, "healthPath": "/health" }
]
Шаг build со статусом ok есть в каждом успешном ответе. Шаг healthcheck добавляется, только когда платформа обращалась к приложению по HTTP: status: "ok" — приложение ответило кодом 2xx или 3xx, status: "warning" — ответило кодом 4xx или 5xx, и деплой всё равно считается успешным, потому что приложение доступно.
Пример ответа
Успешный деплой (JSON):
{
"success": true,
"data": {
"steps": [
{ "step": "runtime", "status": "ok", "duration": 45 },
{ "step": "stop_existing", "status": "ok" },
{ "step": "download", "status": "ok" },
{ "step": "install", "status": "ok", "duration": 12 },
{ "step": "env", "status": "ok" },
{ "step": "platform_env", "status": "ok" },
{ "step": "systemd", "status": "ok" },
{ "step": "start", "status": "ok" },
{ "step": "healthcheck", "status": "ok" }
],
"serviceName": "app",
"status": "running",
"appUrl": "https://app-abc12345.vibecode.bitrix24.tech",
"sleepAfterMinutes": 60,
"source": { "autoSaved": false, "skippedReason": "deploy from external URL" }
}
}
Ошибка получения source.url сохраняет верхнеуровневый код DEPLOY_FAILED, а стабильные детали дублируются в error и в терминальном шаге:
{
"success": false,
"error": {
"code": "DEPLOY_FAILED",
"message": "The source could not be reached within the retry budget.",
"step": "download",
"causeCode": "DOWNLOAD_SOURCE_UNREACHABLE",
"retryable": true,
"attempts": 4
},
"data": {
"steps": [
{
"step": "download",
"status": "error",
"error": "The source could not be reached within the retry budget.",
"causeCode": "DOWNLOAD_SOURCE_UNREACHABLE",
"retryable": true,
"attempts": 4
}
]
}
}
Блок data.source — итог автосохранения исходников. В примерах выше деплой идёт с внешнего URL с заголовком X-Skip-Source-Snapshot, поэтому autoSaved: false, а skippedReason повторяет переданную причину. При деплое из хранилища — source.content или source.versionId — блок выглядит как { "autoSaved": true, "savedVersionId": "vN", "skippedReason": null }. Полный контракт и значения skippedReason — Хранилище исходников.
Поле warnings в успешном ответе появляется, только если есть на что обратить внимание — например, displayName/description не переданы либо не применены (см. «Известные особенности») либо шаг runtime завершился со статусом warning и его сообщения ждут в stdout этого шага. В JSON-режиме это warnings на верхнем уровне ответа рядом с data. В SSE-режиме — то же поле в финальном event: done.
Успешный деплой обычной отдельной виртуальной машины, не защищённой политикой авто-сна для сервера агента или бота, добавляет в конец warnings[] одну запись с префиксом AUTO-SLEEP POLICY:, если снимок на момент начала деплоя содержит числовой sleepAfterMinutes и ни одного включённого повторяющегося окна пробуждения. Отключённые окна предупреждение не скрывают, а хотя бы одно включённое окно или sleepAfterMinutes: null скрывает. Это лишь условие показа deploy-warning: его отсутствие не означает, что сервер не уснёт. При null и действующем расписании применяется платформенный таймаут после окна. Проверка и возвращённое значение относятся к одному стартовому снимку: изменение политики во время деплоя отразится только в следующем ответе. Предупреждение справочное и успешный деплой не отменяет.
Во время долгой подготовки runtime SSE-поток повторяет кадр event: step со статусом running и полем elapsedSeconds. Это число полных секунд с начала шага; поле отсутствует в первом кадре и во всех итоговых кадрах ok/error. Обычный JSON-ответ по-прежнему завершается только в конце операции и поддерживает соединение пробелами, поэтому для содержательного промежуточного прогресса используйте ?stream=true.
SSE-поток при ?stream=true:
event: step
data: {"step":"runtime","status":"running"}
event: step
data: {"step":"runtime","status":"running","elapsedSeconds":15}
event: step
data: {"step":"runtime","status":"ok","duration":45}
event: step
data: {"step":"stop_existing","status":"ok"}
event: step
data: {"step":"download","status":"ok"}
event: step
data: {"step":"install","status":"ok","duration":12}
event: step
data: {"step":"healthcheck","status":"ok"}
event: done
data: {"serviceName":"app","status":"running","appUrl":"https://app-abc12345.vibecode.bitrix24.tech","sleepAfterMinutes":60,"warnings":["displayName and description were not set on this deploy — the Bitrix24 catalog card uses displayName as the title and description as the subtitle, and falls back to the server's technical identifier (slug) without them. The deploy is NOT complete until they are set: re-send the deploy with displayName and description added so the app appears in the catalog with an adequate name."]}
event: source
data: {"autoSaved":false,"skippedReason":"deploy from external URL"}
Пример ответа при ошибке
Деплой упал на шаге install (HTTP 200, success: false):
{
"success": false,
"error": {
"code": "DEPLOY_FAILED",
"message": "npm ERR! Missing dependencies",
"step": "install"
},
"data": {
"steps": [
{ "step": "stop_existing", "status": "ok" },
{ "step": "download", "status": "ok" },
{ "step": "install", "status": "error", "stderr": "npm ERR! Missing dependencies" }
]
}
}
Приложение поднялось, но проверка работоспособности отвечает ошибкой, и платформа успела вернуть
его на права администратора (HTTP 200, success: false):
{
"success": false,
"error": {
"code": "DEPLOY_FAILED",
"message": "App returned HTTP 503 at /health (process is up but responding with error). …",
"step": "healthcheck",
"hardeningRollback": "completed"
},
"data": {
"steps": [
{ "step": "service_user", "status": "ok" },
{ "step": "systemd", "status": "ok" },
{ "step": "start", "status": "ok" },
{ "step": "hardening", "status": "warning", "hardeningRollback": "completed", "stdout": "…" },
{ "step": "healthcheck", "status": "error", "httpCode": 503, "healthPath": "/health", "portOwner": "unknown", "error": "…" }
]
}
}
Что здесь читает программа, не разбирая текст: httpCode: 503 — приложение отвечает по HTTP, то
есть код выложен и процесс поднят. hardeningRollback: "completed" — изоляция снята, приложение
работает с правами администратора. Деплой при этом не засчитан: судит success и статус шага,
а не httpCode. portOwner: "unknown" здесь нормален — зонд владения портом на ответах 5xx не
запускается.
Ошибки
| HTTP | Код | Описание |
|---|---|---|
| 400 | VALIDATION_ERROR |
Нарушена схема запроса: отсутствует source или start, в multipart-режиме не передан файл архива, port вне диапазона 1–65535 или не передан в multipart-режиме, неверный формат архива |
| 400 | NOT_BLACKHOLE |
Сервер в режиме OPEN — Deploy API недоступен |
| 401 | MISSING_API_KEY |
Не передан заголовок X-Api-Key |
| 401 | INVALID_API_KEY |
Ключ не опознан: такой строки на платформе нет |
| 402 | ACCOUNT_FROZEN |
Баланс Вайбкод заморожен |
| 402 | BILLING_EXHAUSTED |
Баланс исчерпан — пополните |
| 403 | INFRA_FORBIDDEN_FOR_COWORK_KEY |
Вызов сделан ключом Cowork/Code — такой ключ работает только с данными, изменяющие операции ему закрыты. Что делать — Проектный ключ для деплоя |
| 403 | WRONG_KEY |
Сервер существует, но вызывающему не принадлежит. Операции над содержимым приложения — выкладка, команда, загрузка файла и чтение логов — открыты по любому из трёх оснований: управляющий ключ сервера, ключ, приложение которого привязано к этому серверу (Application.serverId), либо членство в команде разработки этого сервера (обе роли, «Разработчик» и «Администратор»). Токены доступа и загрузка значка требуют именно управляющего ключа. Управление самой машиной открыто ещё и роли «Администратор» команды. В ответе — hint с восстановлением из двух шагов. Подробнее — Восстановление доступа к серверу |
| 404 | NOT_FOUND |
Сервера с таким id нет или он удалён. Сервер, который существует, но вам не принадлежит, отвечает 403 WRONG_KEY — код различает «нет такого сервера» и «нет прав на него» |
| 403 | AGENT_SLOT_DEPLOY_FORBIDDEN |
Сервер принадлежит AI-агенту — выложить в него приложение нельзя, иначе код агента будет затёрт. Нажмите «Повторить» на карточке агента, чтобы развернуть его заново, либо создайте отдельный сервер под приложение |
| 403 | SERVER_WAKE_BLOCKED |
Сервер спит, а пробуждение запрещено — например, закончился доступ к платформе или действует административная блокировка. Деплой такой сервер не поднимает, повтор не поможет, пока запрет не снят. Разбор запрета — Разбудить сервер |
| 403 | EXTERNAL_COLLABORATOR_KEY_SERVER_MISMATCH |
Вызов сделан ключом внешнего участника разработки, а в адресе указан не тот сервер, для которого ключ выпущен. Такой ключ действует ровно на одном сервере, и ответ не сообщает, существует ли запрошенный сервер. Для работы с другим сервером нужен ключ того внешнего членства |
| 403 | EXTERNAL_COLLABORATOR_NOT_A_MEMBER |
Ключ внешнего участника обращается к своему серверу, но доступа сейчас нет: членство снято, истёк его срок либо для аккаунта Битрикс24, которому принадлежит сервер, отключены внешние участники разработки. Код эти причины не различает и сведений о сервере не раскрывает. Членство перечитывается на каждом запросе, поэтому снятый доступ перестаёт работать сразу |
| 409 | WAKE_IN_PROGRESS |
Спящий сервер уже будит другой запрос. Дождитесь его завершения и повторите деплой |
| 409 | CONFLICT |
Состояние спящего сервера изменилось между проверкой доступа и пробуждением. Прочитайте текущее состояние сервера и исход операции, затем решите, нужен ли повторный деплой |
| 422 | VM_MISSING |
У записи сервера нет виртуальной машины у облачного провайдера, поэтому будить нечего. Сервер нужно заменить: удалить и создать новый. Адрес каждого шага error.hint называет, только если шаг доступен этому запросу, а для недоступного — причину отказа и что изменить. Поле error.userMessage приходит, только когда доступны оба шага |
| 422 | VM_POINTER_LOST |
У записи нет externalId, но разбор облака видит машину под облачным именем этого сервера: связь потеряна, сама машина скорее всего работает и тарифицируется. Удалять сервер и создавать новый НЕ нужно — тогда машина останется работать вообще без записи. Сервер в error не переводится, обратитесь в поддержку за восстановлением связи |
| 502 | WAKE_FAILED |
Во время пробуждения спящего сервера он перешёл в неожиданное состояние. Повторите запрос |
| 502 | PROVIDER_ERROR |
Облачный провайдер вернул ошибку при запуске виртуальной машины спящего сервера. Туннель здесь ни при чём — вызов /repair не поможет |
| 503 | WAKE_TIMEOUT |
Спящий сервер не поднялся за отведённые 6.5 минуты — машина не запустилась или туннель не подключился. Сервер возвращается в sleeping, поэтому повторный запрос безопасен. Если запись операции удалось создать, ответ несёт её id в X-Vibe-Operation-Id и error.operationId; после успешной асинхронной записи исход операции — failed с кодом WAKE_TIMEOUT. См. «Известные особенности» |
| 409 | EXEC_BUSY |
Другая операция уже выполняется на сервере. Ответ несёт заголовок Retry-After и поля retryable: true / retryAfter (секунды) — повторяйте с этим интервалом. Не помогает — POST /v1/infra/servers/:id/unstick, он отвечает 409 OPERATION_IN_PROGRESS, пока замок наблюдается, а сам снимает только exec-мьютекс, переживший собственный TTL. Безусловное DELETE /lock — только после независимо подтверждённого простоя |
| 409 | SERVER_NOT_READY |
Сервер не готов к операции: не запущен, туннель не подключён, либо сервер числится подключённым, но успешный непустой снимок Gateway подтверждает отсутствие живого туннеля. Пустой или недоступный снимок не считается таким подтверждением. В ответе — поле hint с причиной и следующим шагом. Платформа пытается восстановить подтверждённо отсутствующий туннель сама. Для остановленной машины сначала вызовите /start; для stale/DISCONNECTED-туннеля — /repair, затем повторите запрос. Спящий сервер деплой будит сам |
| 409 | AGENT_UPDATE_REQUIRED |
Standalone deploy с extractTo, равным /srv или вложенным в него, требует агент 1.3.22 или новее независимо от runtime и systemd. Galaxy игнорирует extractTo и не использует этот гейт. Standalone-запрос отклоняется до остановки сервиса и очистки каталога; обновите агент и повторите запрос |
| 409 | SNAPSHOT_REQUIRED |
Деплой с внешнего source.url при включённом хранилище исходников. Передайте X-Skip-Source-Snapshot: <причина> или загрузите архив в хранилище и разверните через {source: {versionId}} — см. «Известные особенности» |
| 409 | BASE_VERSION_REQUIRED |
В деплое нет baseVersionId, хотя хранилище исходников непустое и либо его последнюю версию сохранил другой разработчик, либо у сервера есть команда разработки. Если рабочая копия сделана с latestVersionId, повторите запрос с этой базой; иначе сначала скачайте актуальную версию и совместите изменения |
| 409 | SOURCE_VERSION_STALE |
Указанный baseVersionId старше latestVersionId. Скачайте актуальную версию, повторно наложите на неё изменения и лишь затем отправьте деплой с этой базой. Слепая подстановка номера из ответа может затереть чужую работу |
| 400 | INVALID_EXTRACT_TO |
Путь extractTo не подходит: он должен быть абсолютным, из букв, цифр, точки, подчёркивания, дефиса и косой черты, без завершающей косой черты, внутри одного из корней /opt, /srv, /tmp, /root, /var/log, /var/lib, /etc/systemd, /etc/nginx и не может указывать на каталоги ключей и расписаний (/root/.ssh, /etc/cron* и подобные). Для managed standalone static с systemd: true правило уже: вложенный прикладной каталог под /opt или /srv; все пути под /var/lib и /opt/data отвергаются. Для остальных рантаймов и static с systemd: false действует общее правило. Текст ответа называет нарушенное правило. Проверка идёт до того, как деплой что-либо сделает с сервером. На galaxy-приложении поле игнорируется, но проверяется |
| 400 | INVALID_DATA_DIRS |
Каталог из dataDirs не подходит: каждый путь должен быть абсолютным, уже нормализованным, внутри /opt, /srv или /var/lib и иметь собственное имя каталога — сами корни (/opt, /var/lib) отвергаются. Этот же код приходит, если dataDirsRecursive передан без dataDirs или запрошен для /opt/data. Текст ответа называет конкретный путь и что прислать вместо него. Проверка идёт до того, как деплой займёт сервер |
| 400 | GALAXY_DEPLOY_RUNTIME_REQUIRED |
Деплой galaxy-приложения без runtime. Укажите рантайм, например node20 |
| 400 | GALAXY_DEPLOY_CONTENT_ONLY |
Деплой galaxy-приложения с source.url или source.versionId там, где выкладка по ссылке для вас не включена. Пришлите архив встроенным source.content |
| 400 | GALAXY_SOURCE_URL_NOT_ALLOWED |
Ссылка в source.url не прошла проверку до отправки на хост галактики. Причины: адрес ведёт не в хранилище исходников платформы, значение не является абсолютным URL, схема не http и не https, адрес ведёт не в публичную сеть — на локальный, внутренний или служебный узел. Текст ответа называет сработавшее правило, а для стороннего адреса подсказывает замену — сохранить архив версией и выложить по source.versionId. Та же проверка стоит на создании сервера с полем source. Не путайте с GALAXY_DEPLOY_CONTENT_ONLY — там ссылка отклоняется целиком как способ выкладки, здесь не подходит конкретный адрес |
| 400 | SOURCE_VERSION_REQUIRES_APP |
Деплой по source.versionId на сервер, которым владеет другой ключ — менеджмент-ключ или доступ через карточку приложения. Версия ищется в контексте приложения, а ключ-владелец сервера к приложению не привязан. Разверните версию тем ключом, которому принадлежит сервер |
| 404 | SOURCE_VERSION_NOT_FOUND |
Только galaxy-приложение: версии source.versionId нет в депо этого приложения |
| 404 | SOURCE_VERSION_NOT_FOUND |
Только отдельная машина: версии нет ни в депо самого сервера, ни в депо приложения-владельца. Когда сервер принадлежит вызывающему ключу, текст ответа предлагает сначала сохранить архив через POST /v1/infra/servers/:id/sources |
| 410 | SOURCE_VERSION_BYTES_PURGED |
Запись о версии жива, но её байты вычищены из хранилища. Сохраните архив заново — восстановить прежние байты нельзя. Подробнее — Хранилище исходников |
| 400 | GALAXY_HOST_NOT_A_DEPLOY_TARGET |
Деплой пришёл на id машины-галактики (kind: "GALAXY"). Машина несёт контейнеры приложений и целью выкладки не является — обращайтесь по id самого приложения (kind: "GALAXY_APP"), см. Galaxy-приложение |
| 400 | GALAXY_DEPLOY_INVALID_INSTALL |
Деплой galaxy-приложения с многострочной командой install. Для galaxy-приложения install — только однострочная команда, перевод строки ломает сборку |
| 409 | GALAXY_BUILD_BUSY |
На хосте galaxy сейчас собирается другое приложение. Повторите запрос через несколько секунд |
| 409 | GALAXY_APP_BUSY |
На этом galaxy-приложении уже выполняется другая операция. Повторите запрос через несколько секунд |
| 413 | GALAXY_UPLOAD_TOO_LARGE |
Архив исходников превысил лимит загрузки шлюза (до начала сборки). Ошибка детерминированная — повтор того же архива не поможет. Уменьшите архив (исключите node_modules, .git и артефакты сборки — зависимости ставятся на хосте). Если выкладка по ссылке для вас включена, тот же архив можно прислать в source.url — его скачивает хост, и этот потолок на него не распространяется. Иначе уменьшить архив — единственный способ восстановления |
| 413 | INLINE_SOURCE_TOO_LARGE |
Тело со встроенным архивом больше 96 МБ, то есть около 72 МБ самого архива. Решение принимается по заголовку Content-Length до чтения тела, поэтому отказ детерминированный — то же тело повторной отправкой не пройдёт. Трафик это не экономит: платформа принимает тело целиком, и отказ приходит после того, как загрузка закончилась. В error.hint приходят четыре строки — reason, recovery, recoveryAction и note — с готовым переходом на версионный путь. Тот же код приходит в multipart-режиме, когда архив не удалось записать в хранилище потоком и он оказался крупнее 72 МБ, — условия в «Известных особенностях» |
| 413 | PAYLOAD_TOO_LARGE |
Тело запроса превысило краевой лимит платформы — 500 МБ — и отклонено ещё до обработчика. На этом маршруте в краевой лимит упирается multipart-режим: у встроенного base64-архива потолок ниже, и он приходит кодом INLINE_SOURCE_TOO_LARGE. Тот же краевой лимит стоит на остальных маршрутах /v1/, а маршруты AI отдают ошибку в конверте, совместимом с OpenAI. Отличается от GALAXY_UPLOAD_TOO_LARGE, который приходит от шлюза уже во время galaxy-деплоя |
| 502 | GALAXY_BASE_IMAGE_UNAVAILABLE |
Сборка не смогла получить базовый образ — склад образов был недоступен с хоста галактики. Состояние временное: ответ несёт retryable: true и error.hint из четырёх строк — reason, recovery, recoveryAction, note. Слот приложения сломанным не помечается — в том числе при создании сервера сразу с полем source (HTTP-ответ деплоя тогда уже ушёл, причина остаётся в provisionError). Повторяйте тот же деплой с интервалом в несколько минут: окно недоступности обычно короткое, но бывает и долгим. Удалять и пересоздавать приложение не нужно — новый слот попадёт на тот же хост |
| 502 | GALAXY_LOW_DISK |
На хосте галактики кончилось свободное место во время сборки. Состояние повторяемое: ответ несёт retryable: true и error.hint из четырёх строк — reason, recovery, recoveryAction, note. Слот приложения, его контейнер и том /data целы, приложение сломанным не помечается, а уже работающая версия продолжает обслуживать запросы. Повторять деплой имеет смысл после того, как на хосте освободили место: само оно не освобождается, поэтому немедленный повтор упирается в тот же отказ. При создании сервера сразу с полем source приложение сломанным тоже не помечается — причина приходит на карточке сервера в provisionError с категорией RESOURCE |
| 409 | GALAXY_UPLOAD_BUSY |
Другая загрузка использует путь исходников. Повторите тот же деплой не ранее чем через 10 секунд; тело несёт retryable: true, retryAfter: 10 и hint, заголовок — Retry-After: 10. Статус приложения сохраняется |
| 502 | GALAXY_APP_BUILD_FAILED |
Сборка контейнера galaxy-приложения не удалась. Хвост лога сборки — в поле buildLog. Когда причину удалось распознать, ответ дополнительно несёт error.category и error.buildHint — см. «Известные особенности» |
| 502 | GALAXY_APP_START_FAILED |
Контейнер galaxy-приложения собрался, но упал или ушёл в перезапуск по нехватке памяти сразу после старта. Хвост лога — в поле buildLog. Поля error.category и error.buildHint приходят по тем же правилам, что и у GALAXY_APP_BUILD_FAILED |
| 502 | GALAXY_DEPLOY_INTERRUPTED |
Деплой не удалось подтвердить. Если устойчивый статус операции — unknown, ответ несёт retryable: false: сначала прочитайте список операций, состояние сервера и логи, не повторяйте deploy вслепую. Только доказанно провалившаяся операция, чья подсказка разрешает тот же запрос, несёт retryable: true. Поле error.subcause называет форму обрыва: http_window_exhausted, exec_channel_busy, no_this_deploy_container, tail_unreached или source_fetch_interrupted. Поле error.repeated становится true, когда одно приложение прерывалось несколько раз за короткое окно: прекратите цикл deploy, проверьте запуск приложения (команда запуска, порт, зависимости, переменные окружения, лимит памяти), а для заведомо рабочего приложения обратитесь в поддержку с идентификатором сервера. Слот, контейнер и том /data сохранены при любом из этих исходов; не удаляйте и не пересоздавайте приложение |
| 502 | GALAXY_HOST_UNREACHABLE |
Хост galaxy недоступен — туннель хоста не в статусе CONNECTED. Если ответ несёт retryable: false, deploy уже был отправлен, но его устойчивый исход — unknown: сначала прочитайте список операций, состояние сервера и логи, не повторяйте deploy вслепую. Без retryable: false отказ произошёл до отправки сборки — следуйте error.hint, который разрешит повтор после восстановления туннеля. Приложение и его данные сохранены |
| 502 | GALAXY_APP_DEPLOY_FAILED |
Не удалось развернуть galaxy-приложение — непредвиденный сбой деплоя. Повторите запрос |
| 502 | SOURCE_DEPOT_UNAVAILABLE |
Только multipart-режим: хранилище исходников не приняло архив, деплой не стартовал. Тело запроса корректно — повторите тот же запрос без изменений. Раньше такой сбой приходил как 400 VALIDATION_ERROR, поэтому клиент, который ветвится по статусу, теперь попадёт в другую ветку |
| 429 | RATE_LIMITED |
Превышен лимит 10 операций в минуту на сервер. Точное значение — в заголовке x-ratelimit-limit (потолок делится на реплики) |
| 429 | DEPLOY_BACKEND_BUSY |
Слишком много одновременных запросов со встроенным телом: счётчик общий у /deploy, /upload и создания сервера с полем source.content. Ответ содержит заголовок Retry-After: 30 — повторите через указанное время либо передайте архив ссылкой в source.url: ссылка не занимает inline-memory слот, но платформенный relay имеет отдельный ограниченный пул |
| 200 | DEPLOY_FAILED |
Упал один из шагов процесса деплоя. Поле error.step указывает, какой. Для download поля error.causeCode, error.retryable и, когда известно, error.attempts дублируют стабильные значения терминального шага. Ветвитесь по ним, а не по тексту. DOWNLOAD_RELAY_BUSY возникает после успешно завершившегося runtime, если он был передан, но до stop_existing и clean: оркестратор ещё не останавливал юнит и не очищал дерево приложения, однако установщик рантайма уже мог изменить системные зависимости хоста. Эти изменения не откатываются. DOWNLOAD_URL_DENIED, DOWNLOAD_TOO_LARGE, DOWNLOAD_INTEGRITY_MISMATCH и DOWNLOAD_EXTRACT_FAILED повторять без исправления причины не нужно. DOWNLOAD_COMMIT_UNCONFIRMED и DOWNLOAD_OUTCOME_UNCONFIRMED означают устойчивый исход операции unknown, всегда несут retryable: false и требуют сначала прочитать список операций. Если сработал автооткат хардненного запуска, конверт несёт ещё error.hardeningRollback |
| 200 | DEPLOY_TIMEOUT |
Шлюз перестал ждать шаг деплоя, но это не доказывает остановку команды на сервере. Устойчивый исход операции — unknown: сначала выполните действие из error.hint и прочитайте список операций, не повторяйте deploy вслепую |
| 200 | DEPLOY_FAILED с причиной GATEWAY_UNREACHABLE |
Платформа потеряла связь со шлюзом и не знает исход операции. error.hint запрещает слепой повтор и направляет сначала в список операций; unstick допустим после подтверждённого EXEC_BUSY: он гардирован и на живой операции без force отвечает 409. Независимое подтверждение простоя — состоянием сервера и логами — нужно ОБОИМ шагам: и /unstick, чей разрыв туннеля убивает всё, что выполняет агент, и тем более безусловному DELETE /lock после него. Список операций этого не доказывает: он показывает лишь запуски вашего ключа. Отсутствие error.hint.holder тоже не доказательство: чтение держателя fail-open, поэтому «держателя нет» приходит и когда прочитать его не удалось |
| 200 | DEPLOY_CONNECTION_TERMINATED |
Соединение с сервером оборвалось посреди деплоя — агент или туннель отключились. Устойчивый исход операции — unknown: прочитайте список операций, состояние сервера и логи по error.hint, не повторяйте deploy вслепую |
| 200 | DEPLOY_TUNNEL_STALE |
У Gateway нет живого туннеля для сервера, хотя сервер числится подключённым. Обрыв не доказывает исход уже отправленного deploy: сначала прочитайте список операций и выполните read-only проверки из error.hint, не вызывайте /repair и не повторяйте deploy вслепую |
| 502 | RUNTIME_FAILED |
Установка рантайма на сервере провалилась. Попробуйте без runtime или через отдельный /exec |
| 503 | RUNTIME_TIMEOUT |
Установка рантайма не завершилась за 10 минут. Она может завершиться позже — проверьте статус сервера перед повторным запросом. Тело ответа содержит поле retryAfter со значением 30, ответ несёт заголовок Retry-After: 30 — это число секунд до повторного запроса |
HTTP-статус не отражает результат деплоя. На отдельной виртуальной машине (
kind: "STANDALONE") соединение удерживается на всё время деплоя, а статус200отправляется до его начала — поэтому и успешный, и провалившийся деплой приходят с кодом200. Признак отказа — полеsuccess: falseи объектerrorв теле ответа. Проверяйтеsuccessв теле, а не HTTP-статус: клиент, который ветвится по статусу, посчитает провалившийся деплой успешным. У galaxy-приложения (kind: "GALAXY_APP") та же ошибка приходит со статусом502.В потоковом режиме (
?stream=true) провал шага приходит SSE-событиемstepсоstatus: "error"— оно несёт имя провалившегося шага. При исключении или обрыве транспорта отдельное событиеerrorнесётcodeиmessage. При зависшем exec-мьютексе завершающее событиеerrorнесётcode: "DEPLOY_FAILED",stepиhint, но безmessage. Поляsuccessв потоке нет — признак отказа шага — этоstatus: "error"в его событииstep.
Полный список общих ошибок API — Ошибки.
Платформа сообщает порт в `PORT`
Приложение не обязано знать номер порта заранее: платформа сама передаёт его в переменной
окружения PORT — по той же общей конвенции, что Heroku, Render, Railway и Cloud Run. Значение
всегда равно полю port запроса (по умолчанию 3000) — тому же номеру, на который платформа
пробрасывает публичный трафик и по которому проверяет работоспособность.
Поэтому рабочий код — обычный:
require('http').createServer(handler).listen(process.env.PORT, '0.0.0.0')
Как это доставляется:
- Отдельная виртуальная машина (
kind: "STANDALONE") — шагplatform_envпишет рядом с вашим.envотдельный файл.vibe-platform.envс единственной строкойPORT, а systemd-юнит подключает оба файла: сначала ваш, потом платформенный. Systemd применяет их в порядке объявления, поэтому для одноимённой переменной побеждает последний — платформенный. Ваш.envпри этом не читается и не перезаписывается. - Приложение в галактике (
kind: "GALAXY_APP") —PORTпередаётся контейнеру как переменная окружения при запуске, рядом с вашими переменными.
Что стоит учесть:
PORTзарезервирован платформой. Если прислать свойenv.PORT, отличный от поляport, платформа его перекроет и скажет об этом строкой вwarnings[]ответа — вида «Deploy field "port" is 3000; your env.PORT was "9999" and was overridden». Это не ошибка, деплой проходит. Прислать совпадающее значение можно — предупреждения не будет.- Если вы держали в
env.PORTне порт приложения, а что-то другое — порт базы, порт внешнего сервиса, — переименуйте эту переменную. Раньше ваше значение доходило до приложения, теперьPORTпринадлежит платформе и будет перекрыт. Предупреждение вwarnings[]как раз про этот случай. - Приложение, которое читает файл
.envнапрямую (а не переменные окружения процесса),PORTв нём не найдёт: платформенное значение живёт в отдельном файле. Читайтеprocess.env.PORT— популярные загрузчики (dotenvи аналоги) по умолчанию не перетирают уже заданные переменные окружения, поэтому такой код работает без правок. - Свой systemd-юнит (
systemd: false). Юнит в этом режиме создаёте вы, поэтому платформенный файл сам собой не подключится — добавьте в него строкуEnvironmentFile=-<extractTo>/.vibe-platform.envпосле строки с вашим.env. Пока не добавили, побеждает вашenv.PORT, и предупреждение в ответе говорит именно это, а не «значение перекрыто». Файл платформа всё равно пишет, но если записать его не удалось, деплой в этом режиме не прерывается — шагplatform_envвозвращаетwarning. - Уже работающие приложения получат
PORTпри следующем деплое — задним числом окружение запущенного процесса не меняется.
Известные особенности
Потолок multipart-архива зависит от того, ушёл ли архив в хранилище потоком. Прежние 500 МБ действуют, только когда выполнены три условия сразу. Первое — вы обращаетесь к серверу как его прямой владелец, а не через карточку чужого приложения и не менеджмент-ключом. Второе — сервер либо не galaxy-приложение, либо выкладка galaxy-приложения по ссылке включена для вас полностью. Третье — хранилище исходников включено и платформой, и для вашего портала. Если не выполнено хотя бы одно, потоковая запись не включается, архив читается в память целиком, и предел для него становится тем же, что у встроенного base64-тела: около 72 МБ архива, отказ —
413 INLINE_SOURCE_TOO_LARGEс тем жеerror.hint. Путь, который не зависит от этих условий, — сохранить архив версией черезPOST /v1/infra/servers/:id/sourcesи развернуть его по{"source": {"versionId": "vN"}}: там байты едут потоком и упираются в потолок версии, 500 МБ архива. Разбор хранилища — Хранилище исходников.В multipart-режиме версия исходников заводится до старта деплоя. Архив уходит в хранилище потоком, ещё пока запрос читается, и версия сразу получает статус
failed— успешный деплой переключает её наsuccess. Поэтому у неудавшейся выкладки версия остаётся в истории со статусомfailed: это не мусор, а честная отметка «байты приняты, деплой не дошёл». Состав истории — Хранилище исходников. Если само хранилище архив не приняло, деплой не стартует вовсе и запрос возвращает502 SOURCE_DEPOT_UNAVAILABLE— тело корректно, помогает простой повтор.Имя и описание карточки каталога Битрикс24 деплой ЗАПОЛНЯЕТ, но не переименовывает.
displayNameзаписывается, только пока он ещё равен техническому идентификатору сервера,description— только пока оно пусто. Передавайте оба поля при первом деплое: без них в каталоге останется технический идентификатор, а ответ вернётwarnings[]с подсказкой. Если поле уже заполнено, присланное значение не применяется — ответ сообщит об этом отдельной записью вwarnings[]и укажет наPATCH /v1/infra/servers/:id, единственную ручку переименования. Отдельная запись приходит, когда ПРИМЕНЁННОЕ значение потеряло не-ASCII символы по дороге — так ведёт себя Windows PowerShell без явной сериализации в UTF-8: деплой при этом штатный, а строка уезжает в карточку как есть. Про отброшенное поле такой записи не будет — на карточку оно не поехало. Повторный деплой с тем же значением тоже молчит: записи не происходит, а чинить кодировку строки, которая уже лежит в карточке, нужно через переименование.Кириллица в
displayNameиdescriptionиз Windows PowerShell. Отправленная без явной сериализации в UTF-8, она попадает в карточку каталога знаками вопроса (?): байты теряются на стороне клиента, до отправки запроса. Готовый вызов сUTF8.GetBytes— Windows / PowerShell и UTF-8.Не-корневой
healthPathозначает, что деплой проверит и корень. После того как заданный путь ответил, платформа опрашивает/— тот адрес, который открывает Битрикс24. Ответ 5xx на корне завершает деплой ошибкой на шагеhealthcheck: лёгкая ручка вроде/api/healthотвечает 200 даже тогда, когда страница приложения упала, и такой деплой раньше рапортовал об успехе. Ответ 4xx деплой не прерывает — приходит шагapp_rootсо статусомwarning: сервер без страницы на корне (например, только с API) — рабочая форма. Не дошедшая проверка тоже даётwarning, а не ошибку. ПриhealthPathпо умолчанию (/) дополнительного запроса нет.Не-корневой
healthPathпри корневомappUrlдаётwarnings[]. Битрикс24 открывает приложение по адресуappUrlсвязанного приложения, а платформа заполняет его голым origin поддомена — без пути. Если приложение отвечает ТОЛЬКО на подпуть, проверка работоспособности зелёная (она пробила именно подпуть), а фрейм Битрикс24 открывает корень, где приложения нет. Ответ деплоя содержитwarnings[]с подсказкой — действуйте по ней только если корень действительно не обслуживается: пробник на подпути при UI в корне (healthPath: "/health") — нормальная, правильная форма, и перенос такого пути вappUrlнаоборот сломает встройку (фрейм откроет JSON-эндпоинт). Если подпуть действительно неизбежен, поправьте обе половины: деплойте туда И перенесите тот же подпуть вappUrlчерезPATCH /v1/apps/{id}(id — изGET /v1/apps, сопоставьте поappUrl). Проверка живёт только наPOST /v1/infra/servers/:id/deployи только когдаappUrlуказывает на поддомен ЭТОГО сервера. Ответы создания сервера её не несут. Почему корень предпочтительнее, а не просто аккуратнее: после входа зритель возвращается в корень поддомена, а не на подпуть, поэтому приложение с подпутём вappUrlна первом заходе за сессию приземляется на собственный 404. Приложение с роутером на «решётке» вдобавок теряет её на экране пробуждения. Это известное ограничение шлюза, оно чинится отдельно.Статус
warningна шагеtunnel_routingне означает провал деплоя. После успешногоhealthcheckплатформа проверяет, что туннель Gateway маршрутизирует публичный трафик на нужный порт (set_port+ опрос агента). Если порт ещё не определён или агент работает в режиме фиксированного порта, шаг получает статусwarning(неerror) и деплой не прерывается. Публичный URL приложения может около минуты отдавать страницу-заглушку Black Hole, пока сканер портов агента не определит рабочий порт. Заглушка пропадёт сама, если прежний порт освободился. Если на прежнем порту остался живой отвечающий процесс, сканер намеренно удерживает его и сам не переключится — задайте порт явно черезPATCH /portили остановите тот процесс. Если у агента вообще нет автоопределения порта (agentError: NO_SCANNERв тексте шага) — вызовите/repair.Код приложения передаётся через
source.content, а не черезstart/install/preStart. Поля-команды (start,install,preStart) — это короткие shell-команды с лимитом 5000 символов каждое (commandу/exec— 10000). Они не предназначены для содержимого файлов: base64 целого архива туда не помещается и обрезается. Сам код (tar.gz / zip, в том числе один файл) кладите вsource.content— base64-строка архива, до 96 МБ на тело запроса, то есть около 72 МБ архива, — либо вsource.url, а вstartоставляйте только команду запуска (напримерnode server.js). Архив крупнее 72 МБ во встроенном виде не проходит и возвращает413 INLINE_SOURCE_TOO_LARGE: отправляйте его ссылкой вsource.urlили сохранённой версией вsource.versionId, оба пути принимают до 500 МБ архива. На galaxy-приложении оба пути доступны там, где для вас включена выкладка по ссылке, — иначе остаётся уменьшить архив.Четыре стратегии для
.env. Файл пишется в{extractTo}/.env(по умолчанию/opt/app/.env) и приcleanDeploy: trueудаляется перед следующим деплоем:- Передавать
envв каждом/deploy— перегенерируется автоматически (рекомендуется). preserveEnv: true— платформа читает.envдо очистки и записывает его обратно на шагеenv. Если деплой обрывается после создания снимка и перехода к очистке, например на загрузке архива или установке зависимостей, сохранённая копия возвращается на диск по мере возможности. Ошибкаruntimeпроисходит раньше снимка и очистки: платформа не читает прежний.envи не загружает копию обратно, а файл остаётся на месте. Подходит, когда переменных нет под рукой в момент повторного деплоя. Переданный в этом же запросеenvпобеждает: файл пишется заново, сохранённая копия отбрасывается. Только для отдельной виртуальной машины (kind: "STANDALONE").- Хранить
.envвнеextractTo— например,/var/lib/myapp/.env. Эта директория не трогаетсяcleanDeploy. Вstartуказать явный путь. cleanDeploy: false— накопительный (merge) деплой,.envпереживёт, но вместе с ним останутся устаревшие файлы прошлых деплоев.
Стратегии выше относятся к отдельной виртуальной машине. Поведение
envу galaxy-приложения — в отдельной особенности ниже.- Передавать
Часовой пояс окон пробуждения платформа дописывает в
.envсама. Если у отдельной виртуальной машины (kind: "STANDALONE") есть включённые окна пробуждения по расписанию и все они объявлены в одном часовом поясе, деплой добавляет в.envпеременнуюTZс этим поясом — приложение получает её вместе с остальными переменными и считает местное время по ней. Присланный в полеenvсобственныйTZпобеждает: платформа его не перекрывает. ПриpreserveEnv: trueпояс дописывается в восстановленный файл на шагеenv, когда строкиTZв нём не было. Если деплой обрывается после создания снимка и перехода к очистке, но до шагаenv, сохранённая копия возвращается на диск как есть, без подстановки пояса — его добавит следующий успешный деплой. Ошибкаruntimeпроисходит раньше: прежний файл остаётся на месте без чтения и обратной загрузки. Если окна объявлены в разных поясах или окон нет,TZне проставляется. У galaxy-приложения (kind: "GALAXY_APP") пояс не подставляется — контейнер получает только переменные из поляenvэтого запроса.Пробуждение по расписанию только поднимает сервер, но не удерживает его включённым. Если пробуждение по расписанию доступно вашему порталу, включённое повторяющееся окно подходит для работы в заданное время, когда задача без входящей активности успевает завершиться раньше фактического таймаута. Явное числовое
data.sleepAfterMinutesостаётся этим таймаутом; приnullи действующем расписании применяется платформенный запас после окна — по умолчанию 15 минут. Входящие HTTP-запросы и активность по SSH сбрасывают таймер, а cron, фоновые процессы и исходящий трафик приложения — нет. Для непрерывной или более долгой работы нужны невытесняемый тариф,{ "sleepAfterMinutes": null }и отсутствие действующего расписания. Отсутствие deploy-warning приnullили уже созданных окнах само по себе не обещает режим без сна.Приложению, которое само зовёт V1 API Вайбкод, нужен собственный ключ. Если задеплоенное приложение обращается к V1 API Вайбкод (
/v1/deals,/v1/usersи т. п.), оно шлёт в заголовкеX-Api-Keyсвой личный ключvibe_api_. Платформа такой ключ не выдаёт автоматически: пользователь создаёт его в разделе «API-ключи» (/keys, для read-only дашборда — в режиме READONLY с минимальными правами) и передаёт приложению через параметрenv(напримерenv: { "VIBE_API_KEY": "<ключ>" }). Приложение читаетprocess.env.VIBE_API_KEYи шлёт какX-Api-Key. Ответ401(MISSING_API_KEY/INVALID_API_KEY) означает, что приложение не несёт корректного ключа, а не проблему тарифа. Личный ключ обращается к Битрикс24 от имени своего владельца — держитеaccessPolicyсервера вOWNER_ONLY, иначе другие пользователи увидят данные владельца ключа. Если приложение встроено в Битрикс24 как placement и показывает данные per-user — это другой поток авторизации, см. Авторизация в приложении.Автоматическая подгрузка
.envв systemd. В юните прописанEnvironmentFile=-{extractTo}/.env(знак-делает файл опциональным). Приложение через systemd получит переменные само. Но приsystemd: falseили командах изinstall/preStartenv подгружать не будет — передавайте явно.Если смешаны
dependenciesиdevDependencies,npm install --productionне поставит пакеты изdevDependencies. Для cron-скриптов сtsx,ts-nodeи т. п. уберите--productionили добавьте их отдельно черезpreStart.Повторный деплой с тем же
runtimeпереустанавливает его. Опустите параметрruntimeпри повторных деплоях — сэкономит 45+ секунд. Первый деплой сruntimeобязателен для установки.Проверка работоспособности может не успеть. До 10 попыток с интервалом 3 секунды — это около 30 секунд на всё. Если приложение стартует дольше, сделайте эндпоинт
/health, отвечающий 200 сразу, даже до прогрева, или переместите долгую инициализацию вpreStart.Команда
startостаётся на переднем плане и сама держит слушателя. На отдельной виртуальной машине платформа запускает её systemd-юнитом типаsimple: живым юнит считается ровно до тех пор, пока не завершился запущенный процесс. Команда, которая передаёт работу фоновому демону и выходит сама, юнит не удерживает — после её выхода сервис переходит в состояниеinactive, а оставшиеся от неё процессы systemd останавливает. Слушатель исчезает вместе с ними. Пишите команду, которая не возвращает управление:node server.js,python3 app.py,nginx -g 'daemon off;'. Менеджер процессов внутриstart(pm2 start server.js --name app && pm2 saveи любая другая форма запуска с последующим выходом) на этой схеме не работает, даже когда сам менеджер установлен рантаймом.Исходов у такой команды два, и оба от одной причины. Первый — проверка работоспособности не находит слушателя, и деплой завершается ошибкой
attempt 10/10: curl exit=7. Второй — проверка успевает обратиться к приложению, пока фоновый процесс ещё жив, и тогда деплой заканчивается успехом, а приложение перестаёт отвечать через несколько секунд после него. Во втором случае ответ деплоя зелёный, а приложения на порту уже нет. Различает эти два исхода сам результат деплоя: красный со строкойattempt 10/10: curl exit=7— это первый, зелёный с замолчавшим через несколько секунд приложением — второй. ПолеportOwnerшагаhealthcheckисходы НЕ различает:unknownприходит в обоих — в первом зонд владения портом не запускался вовсе (кода в диапазоне 200–399 не было), во втором он отработал и не смог назвать владельцем ваш юнит. Значениеserviceприходит только у команды, которая сама держит порт, поэтомуunknownна зелёном деплое подтверждает: порт держит оставленный фоновый процесс, а не юнит.Успешный деплой означает, что приложение ответило на GET. Шаги
healthcheckиapp_rootзапрашиваютhealthPathи корень методом GET. Готовность маршрутов POST, PATCH и DELETE деплой не проверяет: приложение, отвечающее на них ошибкой, проходит деплой зелёным. Проверяйте такие маршруты собственным вызовом после деплоя.Фоновые приложения тоже должны слушать порт 3000. Шаг
healthcheckвыполняется при каждом деплое — даже если приложение не обслуживает HTTP-трафик (бот, который только опрашивает события, cron-демон, обработчик очереди). Без процесса, слушающего порт 3000, шаг повторяет попытки и завершается ошибкойattempt 10/10: curl exit=7(не удалось подключиться). Поднимите рядом с основной логикой минимальный HTTP-сервер, отвечающий кодом 200 наGET /:Node.js:
require('http').createServer((_req, res) => res.end('ok')).listen(3000) // дальше — основная логика приложенияPython:
import http.server, threading class Health(http.server.BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.end_headers() self.wfile.write(b'ok') def log_message(self, *args): pass threading.Thread( target=lambda: http.server.HTTPServer(('', 3000), Health).serve_forever(), daemon=True, ).start() # дальше — основная логика приложенияЛок на 15 минут. Один
/deployблокирует/execи другие/deploy. Если предыдущий/deployупал на шагеhealthcheckили клиент оборвал соединение по таймауту, последующие/execи/deployбудут возвращать409 EXEC_BUSYдо автоматического истечения лока — то есть до 15 минут с момента старта прерванного деплоя. Ответ409 EXEC_BUSYнесёт заголовокRetry-Afterи поляretryable: true/retryAfter(секунды) — это короткий интервал опроса, повторяйте с ним. Полный верхний предел до снятия лока — вerror.hint.autoExpiresInSeconds. Что делать дальше, решает ВИД зависшего замка. Утёкший exec-мьютекс лечит гардированныйPOST /v1/infra/servers/:id/unstick: запись держателя сop: exec, пережившая свой TTL, снимается им безforce. Серверный лок прерванной ВЫКЛАДКИ таковым не является — законная выкладка живёт дольше своего TTL, поэтому там/unstickбезforceотвечает409, пока замок наблюдается, и остаётся ожидание. ⚠️ Оба вызова требуют независимо подтверждённого простоя — состояния сервера и журнала: разрыв туннеля убивает всё, что выполняет агент. БезусловныйDELETE /v1/infra/servers/:id/lockсрезает ожидание только на реплике, которая ДЕРЖИТ лок: broadcast намеренно не снимает чужой живой deploy. Это последний шаг, и он допустим только после независимо подтверждённого простоя. Пересоздавать сервер не нужно.Долгий деплой и таймауты HTTP-клиента. Полный цикл — особенно с установкой
runtimeи большимnpm install/pip install— реально занимает несколько минут. В JSON-режиме сервер удерживает соединение, отправляя HTTP 200 + chunked-пробелы каждые 15 сек до готовности (поэтомуContent-Lengthотсутствует, а ответ выглядит «висящим»). Пробелы идут внутри уже открытого JSON-объекта, поэтому тело с первого байта выглядит как JSON ({). Окно платформы — 660 секунд: дольше соединение не удерживается. Таймаут HTTP-клиента ставьте не меньше 690 секунд — строго выше окна платформы, потому что равный ему таймаут состязается с самой платформой и ответ можно потерять. Если клиент оборвёт соединение раньше (например,curl --max-time 120или значение по умолчанию в SDK — в ряде клиентов это 300 секунд), запрос на сервере всё равно продолжит выполняться. Симптомы такого обрыва: клиент получил200и заголовки, а тело пустое или не разбирается как JSON — ответ оборвался на удерживающих пробелах. Обрыв на этапе пробуждения выглядит иначе: ответа нет вовсе, потому что статус и заголовки уходят клиенту только после того, как машина проснулась. Сама операция уже видна черезGET /v1/infra/servers/:id/operations: найдите там запуск и после появления id читайте адресный исход выкладки. Если wake успел ответить отказом, его HTTP-ответ несёт id вX-Vibe-Operation-Idиerror.operationId; при fail-open записи id может не быть, и это не доказывает, что запуск не состоялся. Последующие/execи/deployотвечают409 EXEC_BUSYдо завершения серверной части или до истечения 15-минутного лока. Что делать: (а) в curl —--max-time 690или больше, (б) в любом SDK — выставить таймаут запроса ≥ 690 секунд для этого эндпоинта, (в) или используйте?stream=true(SSE) — там события идут построчно по мере прохождения шагов, прокси и SDK таймауты ведут себя стабильнее. Оставшийся после такого обрыва серверный локPOST /v1/infra/servers/:id/unstickне снимет: он гардирован и, пока замок наблюдается, безforceотвечает409, а сам собой снимает только exec-мьютекс, переживший собственный TTL. Остаётся дождаться истечения 15 минут либо перейти к безусловномуDELETE /lock— он остаётся последним шагом и только после независимо подтверждённого простоя. Повторять деплой, не прочитав исход, нельзя: второй запуск ляжет поверх приложения, которое уже могло подняться. Спящий сервер добавляет к этому окну до 6.5 минут: пробелы начинают идти только после пробуждения, поэтому первый деплой после сна встречает до 6.5 минут тишины ещё до 660-секундного окна платформы. Ставьте таймаут с этим запасом либо будите машину заранее вызовом/wakeсwait=true.apt-getвpreStartработает безsudo. Агент и команды деплоя выполняются от имениroot—sudoписать не нужно и это не вызовет ошибку.Само приложение работает под непривилегированной учётной записью. Команды деплоя (
install,preStart) и/exec— отroot, но процесс приложения, который поднимает systemd, запускается от выделенного пользователяvibeapp. Он владеет каталогом деплоя (extractTo, по умолчанию/opt/app) и$HOMEуказывает туда же, поэтому запись внутрь своего каталога, кешиnpm/pipи файлы данных работают без изменений. Что перестаёт работать: запись за пределами своего каталога, если этот каталог не объявлен вdataDirs(см. следующий пункт), подключение к базе, доступной только через рут-сокет (MySQLсauth_socket), и запускdockerиз командыstart. Порт ниже 1024 работает — платформа выдаёт для него отдельное разрешение.Каталог состояния вне
extractToобъявляется полемdataDirs. Чистый деплой стираетextractTo, поэтому долгоживущие данные держат в отдельном каталоге — например/opt/data. Создаёт егоroot(ваш шагinstallилиpreStart, либо сама платформа), поэтому под непривилегированным запуском приложение туда не запишет, пока каталог не объявлен. Передайте в теле деплоя"dataDirs": ["/opt/data/state"]— платформа создаст каталог и передаст его во владениеvibeappна каждом деплое, а при откате хардненинга вернёт владение обратно. Путь — абсолютный, нормализованный, вложенный в/opt,/srvили/var/lib. Сами корни (/opt,/var/lib) отвергаются с кодомINVALID_DATA_DIRSещё до старта деплоя. Каталогов можно объявить до восьми.Передаётся сам каталог, а не его содержимое — этого достаточно, чтобы приложение создавало там свои файлы. Файлы, которые
rootположил внутрь раньше, остаются заroot: приложение не сможет их переписать. Если приложению нужно менять именно их (заранее развёрнутая база, дерево из образа), добавьте"dataDirsRecursive": true.Что важно знать про права. Владелец каталога может удалить или заменить любой файл внутри него — включая те, что положил
root. Поэтому не объявляйте каталог, в котором лежат скрипты, запускаемые потом отroot(например, скрипт восстановления базы, который вы запускаете через/exec), или учётные данные, к которым приложению доступа быть не должно. Держите такое в необъявленном каталоге — именно поэтому в примерах объявляется подкаталог/opt/data/state, а не сам/opt/data: в/opt/dataнаши же рецепты кладут служебные скрипты, запускаемые отroot, и файл с паролем и правами0600. По этой же причинеdataDirsRecursiveдля/opt/dataне принимается вовсе.Если деплой не хардненный (
"hardening": "off","systemd": falseили путьextractToвне разрешённых корней), поле не делает ничего — приложение и так работает с правами администратора. Отдельный шагdata_dirsв этом случае возвращаетwarningи говорит об этом прямо. У галактического приложения поле игнорируется: там долгоживущие данные живут в томе/data.Файл, загруженный через
/uploadпосле деплоя, принадлежитroot. Загрузка выполняется агентом с правами администратора, а каталог деплоя после деплоя принадлежитvibeapp— поэтому дописать или перезаписать такой файл приложение не сможет. Если приложение должно менять загруженный файл, положите его в архив исходников или передайте владение вpreStartследующего деплоя.Если приложение под этим режимом не проходит
healthcheck, деплой автоматически возвращает прежний режим (юнит и владение каталогом) и завершается успешно, добавив шагhardeningсо статусомwarningи хвостом журнала с причиной. Терять деплой из-за хардненинга не придётся.Откат возвращает права и владение каталогом и на этом заканчивается: требование к команде
startон не отменяет. Команда, которая запускает демон и выходит, после отката ведёт себя так же — сервис гаснет вместе с ней независимо от того, под какой учётной записью запущен. Шагhardeningв ответе означает смену режима запуска, а не причину, по которой приложение не отвечает.Чтобы пропустить попытку сразу — передайте
"hardening": "off"в теле деплоя. Это осмысленно для приложений, которым права администратора нужны по существу:nginxв качестве командыstart,MySQLчерез рут-сокет, запуск черезdocker. Приложение на Docker Compose, которое поднимает командаstart, проходит и в этом режиме: шагstop_existingостанавливает контейнер приложения, а после отката командаstartзапускает его заново с правами администратора.Команды
install,preStartиstartвыполняются через POSIX-шелл/bin/sh. Пишите их в POSIX-совместимом синтаксисе. Для остановки при первой же ошибке подойдётset -e, для условий —[ ... ]. Опцияset -o pipefailдоступна только в bash — если она нужна, вызовите bash явно:bash -c 'set -o pipefail; команды'. То же для отдельного скрипта, запускаемого изstart, — начните его строкой#!/bin/bashили запускайте черезbash script.sh.docker-compose-pluginотсутствует в стандартных репозиториях Ubuntu — устанавливайте Docker через официальный скрипт. Пакетdocker-compose-plugin— это компонент Docker CE из репозитория docker.com, которого нет в стандартных Ubuntu 24.04 apt-источниках.apt-get install docker-compose-pluginзавершится ошибкойUnable to locate package. Правильный способ поставить Docker Engine + Compose V2 одной командой:"preStart": "curl -fsSL https://get.docker.com | sh && systemctl enable --now docker"После этого
docker compose(без дефиса) доступен как плагин. Если нужен только Compose V1 (устаревший, только для совместимости), он есть в Ubuntu repos:apt-get install -y docker-compose.cleanDeploy: trueочищает толькоextractTo, не всю систему. По умолчанию удаляется директория/opt/appперед распаковкой нового архива. Системные пакеты (apt), базы данных, файлы в/var/lib/,/etc/,/root/не затрагиваются — они переживут любое число повторных деплоев.Системные пакеты сохраняются при sleep/wake, reboot и repair. Все эти операции сохраняют диск виртуальной машины нетронутым. Повторно устанавливать ПО через
preStartпри каждом деплое не нужно — достаточно сделать это однажды. Последующие деплои безruntimeи без нужныхpreStart-команд найдут пакеты на месте./etc/systemd/system/доступен для записи. Можно создавать кастомные systemd-юниты черезpreStartили/exec:systemctl daemon-reload && systemctl enable --now myservice. Стандартный юнит деплоя (app.service) не конфликтует с пользовательскими юнитами при разных именах.При смене
serviceNameмежду деплоями остановите старый сервис вручную. Шагstop_existingостанавливает только сервис с текущим именемserviceName. Если предыдущий деплой прошёл с именем по умолчаниюapp, а следующий — с именемdedup, то командаsystemctl stop dedupне находит прежнийapp.service. Прежний сервис продолжает работать и удерживает порт приложения — по умолчанию 3000. Новый сервис не может занять порт и уходит в цикл перезапусков. Шагhealthcheckраспознаёт это состояние, включая случай, когда на запрос отвечает кодом 200 прежний процесс: деплой завершается ошибкой. Сообщение об ошибке называет номер порта и процесс-держатель порта, а в случае цикла перезапусков — ещё и состояние сервиса. Команду для освобождения порта —fuser -k <порт>/tcp— оно предлагает не всегда: если новый сервис остался запущенным, а порт держит процесс, который был на нём ещё до начала деплоя, сообщение говорит именно это и команды не даёт, потому что держателем может оказаться нужный вам сервис. Посторонний процесс платформа не завершает. Исключение одно — контейнер Docker Compose самого приложения, его шагstop_existingостанавливает. Освободите порт командойfuser -k <порт>/tcp, остановите прежний сервис командойsystemctl stop <старое-имя>.serviceчерез/execили верните прежнее значениеserviceName, затем повторите деплой. Оговорка: когда диагностическая проверка порта не смогла отработать, ответ со статусом 200–399 засчитывается и деплой завершается успешно.Используйте
debug: trueпри расхождениях/deployи ручного пути. Добавляет шагdebug_infoсsha256sumфайлов иfile -i— помогает сравнить состояние/deployс ручным/upload+/execв байтах. Типичный случай — Tailwind v4 oxide падает на неожиданном UTF-8.Тип архива определяется автоматически, отдельного поля формата нет. Для
source.contentплатформа читает сигнатуру первых байт (PK→ zip,1F 8B→ tar.gz,BZ→ tar.bz2), дляsource.url— расширение пути. Передавать тип явным полем не нужно и негде. Опираться на разбор нераспознанного архива нельзя. Распознанный формат теперь выбирает и способ доставки архива на машину, поэтому «формат не распознали — прочитаем как tar.gz» из обещания превратилось в последнюю попытку прежнего пути доставки. Собирайте архив как tar.gz (tar -czf app.tar.gz -C ./my-app .) или zip (zip -r app.zip .) — тогда результат не зависит от того, какой путь доставки выберет платформа.Для загрузки кода предпочтителен tar.gz. Оба формата распаковываются, но zip, собранный средством
Compress-Archiveиз PowerShell, хранит пути с обратной косой чертой\. Вложенные файлы из такого архива могут оказаться в корнеextractToвместо своих папок, и часть кода не попадёт в нужное место. Шагnormalize_windows_pathsприводит такие пути к обычному виду, но чтобы исключить проблему, соберите код в tar.gz:tar -czf app.tar.gz -C ./my-app .. Если нужен zip — упакуйте его Unix-инструментом:zip -r app.zip ..Архив
source.urlдля отдельной VM в новом режиме скачивает платформа. Для серверов, включённых в rollout, она проверяет конечный адрес каждого соединения и редиректа, читает архив потоком и передаёт на VM ограниченными блоками через существующий туннель. Поэтому доступность источника не зависит от egress/NAT конкретной VM. Временные DNS/TCP/TLS/HTTP/body-отказы получают до четырёх попыток с backoff в общем бюджете пяти минут. Распаковка выполняется один раз после проверки размера и SHA-256. До включения либо на агенте без relay используется совместимая VM-download ветка. При выключенном kill switch она сохраняет исторический URL/timeout/error-контракт. Стабильные очищенные ошибки относятся к новому режиму.Крупные inline-архивы конкурируют за слот.
source.contentбольше ~10 КБ держит запрос в памяти на всё время деплоя, поэтому такие запросы ограничены небольшим числом одновременных (по умолчанию 2 на бэкенд). При превышении приходит429с подсказкой переключиться наsource.urlилиsource.versionId: они не занимают этот inline-memory слот, но пул загрузки URL через платформу ограничен отдельно. При насыщении он возвращаетDOWNLOAD_RELAY_BUSYпосле успешно завершившегосяruntime, если он был передан, но доstop_existingиclean: оркестратор ещё не останавливал юнит и не очищал дерево приложения. Установщик рантайма уже мог изменить системные зависимости хоста, и эти изменения не откатываются. Мелкие inline-архивы иversionIdслот не занимают.Деплой с внешнего URL требует снимок исходников или явный отказ. Когда на портале включено хранилище исходников, деплой с
source.url, который указывает не на хранилище Вайбкод, возвращает409 SNAPSHOT_REQUIRED— так история версий приложения не теряется при деплое со стороннего адреса. Есть два пути. Передать заголовокX-Skip-Source-Snapshot: <причина ≤200 символов>— деплой продолжится без сохранения снимка. Либо сначала загрузить архив черезPOST /v1/apps/:id/sourcesи развернуть его через{ "source": { "versionId": "vN" } }— тогда платформа сохранит версию. Источник из самого хранилища —source.versionIdилиsource.content— этого заголовка не требует. Подробнее — Хранилище исходников.Galaxy-приложению свой
Dockerfileне нужен — платформа генерирует его сама. Из полей деплояruntimeиport, а для рантаймов кромеstaticтакжеstart, платформа собираетDockerfileс базовым образом и строкойEXPOSE <port>. Для остальных рантаймовCMDвыводится изstart; static получает платформенную команду nginx и игнорируетstart. Класть собственныйDockerfileв архив не требуется и не нужно. Это касается только galaxy-приложения — полеcreatedViaравноgalaxy.Ошибка сборки
published no host port for <port>означает, что контейнер не удержался наport, а не отсутствиеEXPOSE. У galaxy-приложения этот текст приходит, когда контейнер упал на старте, командаstartневерна или приложение слушает другой порт — то есть наportникто не отвечает. Поправьтеstartиportтак, чтобы приложение действительно слушалоport. ДобавлятьEXPOSEвручную не нужно: платформа уже выставила его изport.Переменные
envgalaxy-приложения подставляются в контейнер при запуске, а не вшиваются в образ. Платформа передаёт их флагомdocker run --envна старте контейнера, поэтому секреты изenvне попадают в слои образа и не видны в его истории. Это касается только galaxy-приложения — полеcreatedViaравноgalaxy.Деплой galaxy-приложения без пользовательского
envсбрасывает переменные окружения и возвращаетwarnings[]. Контейнер пересоздаётся при каждом деплое, а переменные передаются только аргументами его запуска в текущем запросе. Если в/deployполеenvне передано, передано как{}или содержит только платформенныйPORT, контейнер владельца стартует без прежних пользовательских переменных. Успешный ответ предупреждает об этом и просит передавать полный наборenvв каждом деплое.preserveEnvдля galaxy-приложения игнорируется и предупреждение не скрывает. Исключение — деплой участника команды разработки: платформа сохраняет уже заданные владельцем переменные, поэтому предупреждения о сбросе нет. Присланный участникомenvигнорируется с отдельной строкой. Стратегии сохранения.env—preserveEnv, файл внеextractTo,cleanDeploy: false— доступны только для отдельной виртуальной машины.Архив из хранилища исходников хост галактики скачивает сам, и причина отказа называется отдельно. Когда деплой идёт снимком
source.versionId, а архив собран как tar.gz, машина скачивает его сама и на месте сверяет размер и контрольную сумму с сохранённой версией. Если скачать или распаковать не удалось, приходит502 GALAXY_APP_BUILD_FAILED, а хвостbuildLogоткрывается маркером причины:UPLOAD_DOWNLOAD_FAILED— не скачалось,UPLOAD_EXTRACT_FAILED— не распаковалось,UPLOAD_NO_SPACE— на хосте нет места. Маркеры ложатся вerror.categoryкакSOURCE_FETCH_FAILED,SOURCE_EXTRACT_FAILEDиRESOURCE— что делать с каждой причиной, сказано в таблице категорий ниже. Единственное, чего там нет: на нехватке места уменьшать архив бессмысленно, место занято не вами. Скачивание хостом включается по аккаунтам, а zip и архивы сверх потолка ссылки идут прежним путём — там причина приходит одним сообщением о неудачной распаковке.Ошибки сборки и старта galaxy-приложения приходят с разбором причины. У ответов
502 GALAXY_APP_BUILD_FAILEDи502 GALAXY_APP_START_FAILEDплатформа разбирает лог сборки и, когда причина распознана, добавляет в объектerrorдва поля:category— машинный код причины,buildHint— рекомендация на языке ключа. Возможные значенияcategory:Значение Что произошло NO_SOURCEСлот приложения создан, код так и не загружен DEPLOY_INCOMPLETEЗагрузка кода не завершилась — сборка прервалась или хост был недоступен SOURCE_FETCH_FAILEDАрхив не удалось скачать на машину — ссылка протухла, связь оборвалась или содержимое не совпало с сохранённой версией. Отправьте выкладку ещё раз: ссылка выписывается заново SOURCE_EXTRACT_FAILEDАрхив скачан, но распаковать его не удалось — битый архив либо члены, ведущие за пределы каталога распаковки EMPTY_CONTEXTАрхив распаковался в пустой контекст — файлов приложения в нём нет BUILD_TIMEOUTСборка не уложилась в отведённое время PORT_NOT_PUBLISHEDКонтейнер не занял port— приложение завершилось сразу при стартеRUNTIME_CRASHКонтейнер собрался, но не остался живым после старта RUNTIME_OOMКонтейнер исчерпал память после старта — упёрся в лимит памяти галактики BUILD_OOMСборке образа не хватило памяти — шаг установки упёрся в лимит памяти сборки, это половина памяти хоста галактики. Подготовьте тяжёлую компиляцию заранее или выберите тариф галактики побольше. Отказ терминальный — платформа его не повторяет RESOURCEНа хосте не осталось свободного места — при скачивании архива на машину либо во время самой сборки. По HTTP нехватка места на сборке приходит отдельным кодом 502 GALAXY_LOW_DISK, повторяемым и не помечающим приложение сломанным, а на карточке сервера остаётся эта категорияBASE_IMAGE_UNAVAILABLEСклад базовых образов был недоступен на шаге получения основы для сборки. Состояние временное — тот же деплой проходит после повтора HOST_UNAVAILABLEХост галактики был недоступен во время сборки NO_PACKAGE_JSONКоманда установки задана явно, а package.jsonв корне архива нетMODULE_NOT_FOUNDМодуль не найден — путь запуска в startили отсутствующая зависимостьINSTALL_FAILEDУстановка зависимостей завершилась ошибкой INSTALL_AUTHРеестр пакетов вернул 401 или 403 — нет доступа к приватному пакету INSTALL_REGISTRY_UNAVAILABLEКаталог пакетов Python или Node.js либо каталог Debian не ответил во время установки зависимости или системного пакета. Отказ терминальный — платформа его не повторяет NATIVE_PREBUILD_UNAVAILABLEГотовую сборку зависимости с машинным кодом скачать не удалось: внешний источник не ответил, а собрать её из исходников в образе приложения нечем. Состояние временное — тот же деплой проходит после повтора. Отказ терминальный: платформа его не повторяет, повторите деплой сами через несколько минут Когда причину распознать не удалось, обоих полей в ответе нет — остаются
code,messageиbuildLog. Для ветвления в коде используйтеcategory, для показа пользователю —buildHint.У ответа
502 GALAXY_HOST_UNREACHABLEесть полеerror.hintс планом восстановления. Это объект из четырёх строк:reason— почему хост сейчас недоступен,recovery— что делать,recoveryAction— следующий безопасный вызов,note— оговорка о том, что статус хоста в выдаче может отставать от реального состояния туннеля. Для deploy есть две ветки. Приretryable: falseсборка уже была отправлена, а устойчивый исход —unknown:recoveryActionведёт в список операций, не повторяйте deploy вслепую. Безretryable: falseотказ произошёл до отправки сборки, и подсказка разрешает повтор после восстановления туннеля. Удалять и пересоздавать приложение не нужно — слот, контейнер и том/dataцелы.Спящую отдельную виртуальную машину деплой будит сам и дожидается её в том же запросе. Если сервер (
kind: "STANDALONE") в статусеsleeping, деплой запускает пробуждение и ждёт готовности до 6.5 минут: 5 минут на запуск машины и подключение туннеля, затем ещё 90 секунд ожидания без перезагрузки. Уложился — деплой идёт дальше тем же запросом, отдельный вызов/wakeне нужен. Не уложился — приходит503 WAKE_TIMEOUT, а сервер возвращается вsleeping— повторный запрос безопасен и начинает пробуждение заново. Запрет на пробуждение деплой не обходит: при замороженном балансе запрос отбивается ещё до выкладки кодом402 ACCOUNT_FROZEN, а при остальных причинах — например, окончании доступа к платформе или административной блокировке — приходит403 SERVER_WAKE_BLOCKED. Повтор в обоих случаях не поможет, пока состояние не изменится. Так же ведут себя/exec,/uploadи/logs.Спящую галактику деплой будит сам, и на первый запрос может не хватить времени. Если хост галактики спит, деплой запускает пробуждение в фоновом режиме и ждёт подключения туннеля примерно 4 минуты. Холодный хост чаще всего укладывается в это окно, и деплой проходит одним запросом. Если не уложился до отправки сборки, приходит
502 GALAXY_HOST_UNREACHABLEбезretryable: false, а хост продолжает подниматься — платформа не откатывает пробуждение; после восстановления туннеля действуйте поerror.hint. Если ответ несётretryable: false, сборка уже была отправлена и её исходunknown: сначала прочитайте список операций, состояние сервера и логи, не повторяйте deploy вслепую. Заморозку по биллингу и остановленный вручную хост платформа не будит — там повтор не поможет, пока состояние не изменится.
Путь загрузки занят
Другая загрузка использует путь исходников. Синхронный деплой отвечает 409 GALAXY_UPLOAD_BUSY с error.retryable: true, error.retryAfter: 10, таким же заголовком Retry-After: 10 и error.hint. Подождите не менее 10 секунд и повторите тот же POST /v1/infra/servers/:id/deploy для этого приложения.
Сам отказ сохраняет статус приложения и уже работающую версию. Если сборка началась в фоне после создания сервера, новое приложение сначала остаётся в provisioning: ответ сервера и карточка в кабинете показывают buildHint с советом повторить тот же деплой. Если попытка остаётся зависшей 20 минут, очередная проверка в течение следующих 5 минут переводит приложение в error, сохраняя причину занятого пути и buildHint. Повторите тот же деплой и после этого перехода: новая попытка возвращает такой слот в provisioning. Данные /data сохраняются. Фоновая сборка исходников после создания сервера автоматически не повторяется. Hermes повторяет свою загрузку не раньше чем через 3 минуты; прежний лимит ожидания 30 минут завершает попытку с сохранением причины занятого пути и подсказки. Удалять приложение, снимать блокировку или вызывать /unstick не нужно.