Для 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 — личный ключ

Terminal
# 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-архив (минимальный набор полей)

Terminal
# Канонический 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 — произвольный стек без рантайма

Terminal
# Деплой без 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-приложение

Terminal
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 — личный ключ

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-приложение

javascript
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-приложения короче, чем у отдельной машины, и состоит максимум из двух шагов:

JSON
"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):

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 и в терминальном шаге:

JSON
{
  "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):

JSON
{
  "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):

JSON
{
  "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) — тому же номеру, на который платформа пробрасывает публичный трафик и по которому проверяет работоспособность.

Поэтому рабочий код — обычный:

JavaScript
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 удаляется перед следующим деплоем:

    1. Передавать env в каждом /deploy — перегенерируется автоматически (рекомендуется).
    2. preserveEnv: true — платформа читает .env до очистки и записывает его обратно на шаге env. Если деплой обрывается после создания снимка и перехода к очистке, например на загрузке архива или установке зависимостей, сохранённая копия возвращается на диск по мере возможности. Ошибка runtime происходит раньше снимка и очистки: платформа не читает прежний .env и не загружает копию обратно, а файл остаётся на месте. Подходит, когда переменных нет под рукой в момент повторного деплоя. Переданный в этом же запросе env побеждает: файл пишется заново, сохранённая копия отбрасывается. Только для отдельной виртуальной машины (kind: "STANDALONE").
    3. Хранить .env вне extractTo — например, /var/lib/myapp/.env. Эта директория не трогается cleanDeploy. В start указать явный путь.
    4. 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/preStart env подгружать не будет — передавайте явно.

  • Если смешаны 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:

    javascript
    require('http').createServer((_req, res) => res.end('ok')).listen(3000)
    // дальше — основная логика приложения

    Python:

    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.

  • Переменные env galaxy-приложения подставляются в контейнер при запуске, а не вшиваются в образ. Платформа передаёт их флагом 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 не нужно.

Смотрите также