Выполнить команду

POST /v1/infra/servers/:id/exec

Выполняет shell-команду на BLACKHOLE-сервере через агент туннеля. Полный стандартный вывод (stdout), поток ошибок (stderr) и код возврата передаются клиенту. По умолчанию ответ — JSON после завершения команды (рекомендуется для AI-агентов и скриптов). Если нужен построчный прогресс — передайте ?stream=true и читайте SSE-события stdout/stderr/exit. Отказ при выполнении команды приходит в потоковом режиме событием error.

Для AI-агентов и MCP-клиентов обязательно используйте JSON-режим (без ?stream=true) — SSE они разбирать не умеют.

Параметры

Параметр В Тип Обяз. По умолч. Описание
id path string (UUID) да ID BLACKHOLE-сервера, status: running, blackholeStatus: CONNECTED
stream query string нет false true — SSE-режим, иначе — JSON-ответ

Поля запроса (body)

Поле Тип Обяз. Описание
command string да Shell-команда. 1–10 000 символов
timeout number нет Таймаут выполнения в секундах: 1–600. По умолчанию 300
workdir string нет Рабочая директория. До 500 символов
env object нет Переменные окружения: { "KEY": "value" }. Только строковые значения

Примеры

curl — личный ключ

Terminal
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/exec?stream=false" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command": "ls -la /opt/app", "timeout": 30}'

curl — OAuth-приложение

Terminal
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/exec?stream=false" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"command": "npm ci --production", "workdir": "/opt/app", "timeout": 180}'

JavaScript — личный ключ

javascript
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/exec?stream=false`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      command: 'node -v',
      timeout: 10,
    }),
  }
)
const { data } = await res.json()
console.log(`exit ${data.exitCode} за ${data.duration}s:\n${data.stdout}`)

JavaScript — OAuth-приложение

javascript
// Команда с переменными окружения и рабочей директорией
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/exec?stream=false`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      command: 'npx tsx scripts/seed.ts',
      workdir: '/opt/app',
      env: { DATABASE_URL: 'postgresql://localhost/mydb' },
      timeout: 60,
    }),
  }
)

Поля ответа

Поле Тип Описание
success boolean true если команда выполнилась (включая ненулевой exitCode)
data.exitCode number Код возврата процесса
data.stdout string Стандартный вывод команды
data.stderr string Поток ошибок команды
data.duration number Время выполнения в секундах
data.truncated boolean true если stdout/stderr обрезаны по размеру (5 МБ на поток)

Пример ответа

JSON
{
  "success": true,
  "data": {
    "exitCode": 0,
    "stdout": "Linux epd65hdv07p8g89c0c06 6.8.0-107-generic #107-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 13 19:51:50 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux\n",
    "stderr": "",
    "duration": 5,
    "truncated": false
  }
}

Пример ответа при ошибке

409 — на сервере уже выполняется другая операция. Это отказ до запуска команды, поэтому HTTP-статус отражает результат:

JSON
{
  "success": false,
  "error": {
    "code": "EXEC_BUSY",
    "message": "Another operation is running on this server"
  }
}

Ошибки

Ошибки эндпоинта делятся на две группы, и отдаются они по-разному. Отказ до запуска команды приходит с настоящим HTTP-статусом. Отказ во время выполнения приходит в теле ответа.

Ошибки до запуска команды

HTTP Код Описание
400 VALIDATION_ERROR Нарушена схема запроса (пустая команда, неверный таймаут, workdir длиннее 500 символов)
400 NOT_BLACKHOLE Сервер в режиме OPEN — Deploy API недоступен
400 COMMAND_TOO_LONG Команда длиннее 10 000 символов. В ответе hint: большие данные и скрипты передавайте через `/upload`, затем bash /путь/скрипт.sh
401 MISSING_API_KEY Не передан заголовок X-Api-Key
401 INVALID_API_KEY Неверный или просроченный API-ключ
404 NOT_FOUND Сервер не существует, удалён или принадлежит другому API-ключу
409 EXEC_BUSY На сервере уже выполняется другая /exec или /deploy. Подождите или снимите лок через `/lock`. Если EXEC_BUSY сохраняется после снятия лока — расклиньте канал через POST /v1/infra/servers/:id/unstick (см. ниже)
409 SERVER_NOT_READY Сервер не готов к операции: не запущен или туннель не подключён. В ответе — поле hint с причиной и следующим шагом. Разбудите сервер или вызовите `/repair` и повторите запрос. Случай «числится подключённым, но у Gateway нет живого туннеля» на этом маршруте приходит как 502 TUNNEL_NOT_FOUND (см. ниже)
429 RATE_LIMITED Превышен лимит 10 операций в минуту на сервер

Ошибки во время выполнения

HTTP-статус не отражает результат команды. На отдельной виртуальной машине (kind: "STANDALONE") соединение удерживается на всё время выполнения, а статус 200 отправляется до его начала — поэтому и успешная, и провалившаяся команда приходят с кодом 200. Признак отказа — поле success: false и объект error в теле ответа. Проверяйте success в теле, а не HTTP-статус: клиент, который ветвится по статусу, посчитает провалившуюся команду успешной. У galaxy-приложения (kind: "GALAXY_APP") та же ошибка приходит со статусом 502.

Код Описание
EXEC_TIMEOUT Превышен timeout (или стандартные 300 секунд). Агент завершает процесс-группу принудительно (SIGKILL, без grace-паузы). В ответе hint с путями решения — см. секцию «Фоновые задачи» ниже
EXEC_FAILED Ошибка выполнения на агенте
EXEC_BUSY Занят собственный exec-мьютекс агента — он уже выполняет другую команду. Отличается от платформенного лока (409 до старта): приходит уже во время выполнения, в теле как success: false (на STANDALONE — с HTTP 200, на GALAXY_APP — с 502). В ответе hint с путём восстановления через `/unstick`

Отказ связи с туннелем приходит в том же виде — со своим кодом в поле error.code (например TUNNEL_NOT_FOUND или GATEWAY_TIMEOUT: …). Часть gateway-кодов несёт детали после двоеточия (GATEWAY_UNREACHABLE: …, GATEWAY_TIMEOUT: …), поэтому сопоставляйте error.code по префиксу, а не по точному совпадению. В потоковом режиме (?stream=true) эти же ошибки приходят SSE-событием error. Данные события несут code и message, поля success в них нет — признаком отказа служит само событие error.

Полный список общих ошибок API — Ошибки.

Известные особенности

  • Блокировка на уровне сервера. Пока идёт /exec или /deploy, второй такой вызов вернёт 409 EXEC_BUSY. Если предыдущий /deploy был прерван (например, упал на шаге healthcheck или клиент оборвал соединение по таймауту), серверный лок продолжает держаться до автоматического истечения — для /deploy это до 15 минут, для /exec — пока не истечёт timeout команды плюс 90 секунд. Снять лок сразу можно вызовом `DELETE /v1/infra/servers/:id/lock`, после чего /exec снова работает. Пересоздавать сервер не нужно.
  • Максимальный размер stdout/stderr — 5 МБ на поток. Если вывод команды больше, приходит truncated: true и хвост обрезается. Команда при этом отработала, exitCode корректный. Для большого вывода перенаправляйте в файл: command: "my-cmd > /opt/app/output.log 2>&1" и потом читайте через /exec cat /opt/app/output.log.
  • .env из /deploy не загружается автоматически. Systemd подгрузит .env при старте сервиса, но не для разовых команд через /exec. Если нужны DATABASE_URL / API-ключи — передавайте в поле env: { env: { DATABASE_URL: "..." } }.
  • Два таймаута в сумме дают timeout + 30 секунд. Агент убивает процесс ровно по timeout. Gateway ждёт ещё 30 секунд на финальные события и только потом отдаёт EXEC_TIMEOUT.
  • Поддержание соединения в JSON-режиме. Если команда выполняется дольше 15 секунд, сервер периодически шлёт пробелы в тело ответа — это предотвращает таймауты nginx и промежуточных прокси. JSON-парсер игнорирует начальные и конечные пробелы, так что клиент просто прочитает корректный JSON по окончании.
  • Клиентский таймаут: убедитесь, что он межбайтовый, а не общий. Keepalive-пробелы сбрасывают таймауты «на тишину», поэтому классические сокет-таймауты Python (urlopen(..., timeout=300), read-таймаут requests) сами по себе долгую команду в JSON-режиме не оборвут. Обрывают две другие вещи. Первая — клиенты и обёртки с настоящим wall-clock-дедлайном на весь запрос (например, aiohttp с ClientTimeout(total=…) или собственный дедлайн агент-фреймворка): keepalive против них бессилен. Вторая — серверный параметр timeout самой команды (по умолчанию 300 секунд): по его истечении агент завершает процесс и приходит EXEC_TIMEOUT независимо от клиентских настроек — не путайте с обрывом на своей стороне. Практика: в Python задавайте раздельные таймауты — requests.post(url, json=body, headers=headers, timeout=(10, 60)) — 10 секунд на connect и 60 на паузу между байтами. Если клиент всё же оборвал соединение, лок на сервере держится ещё до timeout + 90 секунд (EXEC_BUSY для повторных вызовов). Команды дольше нескольких минут надёжнее запускать фоновой задачей — см. «Фоновые задачи» ниже.

Расклинивание зависшего канала

Если после `DELETE /v1/infra/servers/:id/lock` вызов /exec всё ещё возвращает EXEC_BUSY/deploy — падает с кодом DEPLOY_FAILED и сообщением «Another command is running»), значит завис exec-мьютекс на стороне агента — фоновый процесс держит канал открытым, поэтому серверный лок снят, а канал остаётся занятым. POST /v1/infra/servers/:id/unstick снимает серверный лок И разрывает туннель агента, который при переподключении завершает зависшую команду и освобождает мьютекс. Перезагрузка сервера не выполняется.

Если в момент вызова на сервере всё ещё выполняется легитимная операция (деплой, exec или харден), эндпоинт по умолчанию отвечает 409 OPERATION_IN_PROGRESS и НЕ трогает её — иначе он оборвал бы работающую команду. Расклинивать нужно только зависший канал. Если вы уверены, что он действительно завис, повторите с ?force=true.

Ответ: { "success": true, "data": { "backendLockReleased": true, "agentBounced": true, "reconnected": true } }. Для galaxy-хостов и galaxy-приложений расклинивание не поддерживается (409 GALAXY_UNSTICK_UNSUPPORTED).

Фоновые задачи

timeout у /exec ограничен 600 секундами, а по его истечении агент завершает всю процесс-группу принудительно (SIGKILL, без grace-паузы). Операции длиннее — установка пакетов, восстановление дампа БД, тяжёлая сборка — запускайте фоновой задачей: /exec вернётся мгновенно, канал освободится (второй вызов не упрётся в EXEC_BUSY), а процесс продолжит работать независимо от HTTP-соединения.

Рекомендуемый способ — транзиентный systemd-юнит

JSON
{ "command": "systemctl reset-failed restore-db 2>/dev/null; systemd-run --unit=restore-db /bin/bash /opt/data/restore.sh", "timeout": 30 }

Задача получает собственный cgroup и живёт независимо от exec-сессии. Управление — стандартными вызовами:

  • статус: { "command": "systemctl is-active restore-db" }active (идёт), inactive (завершилась успешно), failed (упала).
  • логи: journald подхватывает вывод юнита автоматически — читайте через `GET /v1/infra/servers/:id/logs?service=restore-db`.
  • systemctl reset-failed <имя> перед повторным запуском — иначе systemd откажет в создании юнита с именем упавшей задачи.

Лёгкий вариант — фон с редиректом вывода

JSON
{ "command": "(cd /opt/app && npm run build > /tmp/build.log 2>&1 &)", "timeout": 10 }

Редирект обоих потоков в файл обязателен — он освобождает пайпы exec-сессии. Прогресс: { "command": "tail -20 /tmp/build.log" }.

Анти-паттерн — голый `nohup`

nohup cmd & без редиректа не работает: в пайпе (а не терминале) nohup не перенаправляет вывод, фоновый процесс наследует пайп exec-сессии, агент ждёт его закрытия до самого timeout — и затем принудительно завершает всю процесс-группу, включая ваш «фоновый» процесс. Всегда добавляйте > файл 2>&1.

Секреты (пароли БД и т. п.) передавайте через поле env, а не внутри command — команды журналируются агентом (первые 200 символов). Помните также, что шаги install/preStart у `/deploy` имеют собственный таймаут 300 секунд — тяжёлые установки выносите в разовый /exec или фоновую задачу, а не в install-скрипт.

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

Полный деплой приложения

POST /v1/infra/servers/:id/deploy

Полный цикл деплоя за один запрос: остановка предыдущего сервиса → скачивание архива → распаковка → установка рантайма → установка зависимостей → запись .env → команды preStart → создание systemd-юнита → запуск → проверка работоспособности. Используйте вместо цепочки /upload + /exec — быстрее и атомарно. По умолчанию ответ — JSON (рекомендуется для AI-агентов). Для построчного прогресса передайте ?stream=true — сервер вернёт поток SSE (Server-Sent Events) с событиями на каждом шаге.

Для AI-агентов и MCP-клиентов обязательно используйте JSON-режим (без ?stream=true).

Galaxy-приложение — деплой сразу после создания

Если сервер — galaxy-приложение (поле createdVia равно galaxy в ответе `POST /v1/infra/servers`), деплой запускают сразу после создания, не дожидаясь blackholeStatus: "CONNECTED" — контейнер собирается самим деплоем. Модель размещения, жизненный цикл и стоимость — на странице Galaxy-приложение.

Отличия деплоя для galaxy-приложения:

  • runtime и start обязательны. Без runtime запрос вернёт 400 GALAXY_DEPLOY_RUNTIME_REQUIRED.
  • Источник — только встроенный source.content (архив в base64). Варианты source.url и source.versionId для galaxy-приложения отклоняются с 400 GALAXY_DEPLOY_CONTENT_ONLY.
  • Dockerfile в архиве не требуется — платформа сгенерирует его сама из runtime, port и start. Если положить свой Dockerfile в архив, он будет проигнорирован (платформа всегда генерирует свой). См. «Известные особенности» ниже.
  • Рабочая директория контейнера — /opt/app (та же, что и extractTo у обычной виртуальной машины): код копируется туда, поэтому работают и относительная команда (start: "node server.js"), и абсолютный путь (start: "node /opt/app/server.js"). Путь /app тоже ведёт на /opt/app — это символьная ссылка для обратной совместимости. То есть один и тот же start подходит и для VM-, и для galaxy-деплоя.

Жизненный цикл galaxy-приложения — на странице Galaxy-приложение. Сценарий «деплой одним запросом при создании» — на странице Создать сервер.

Параметры

Параметр В Тип Обяз. По умолч. Описание
id path string (UUID) да ID BLACKHOLE-сервера, status: running, blackholeStatus: CONNECTED
stream query string нет false true — SSE-режим, иначе — JSON-ответ

Поля запроса (body — JSON)

Поле Тип Обяз. По умолч. Описание
source object да Источник кода — ровно один из трёх вариантов ниже (url, content или versionId)
source.url string да (или content / versionId) HTTPS-URL архива с кодом. До 500 МБ
source.content string да (или url / versionId) Base64-строка архива. До 500 МБ на тело запроса
source.versionId string да (или url / content) ID снапшота из депо приложения, например v3 — см. Хранилище исходников
start string да Команда запуска приложения (пример: cd /opt/app && node server.js)
port number нет 3000 Порт, на котором приложение слушает. Всегда 3000 для BLACKHOLE — если не передан, подставляется 3000. (В multipart-режиме порт указывать обязательно, см. таблицу ниже.)
extractTo string нет /opt/app Директория распаковки
runtime string нет ID рантайма: node20, python311, php83, static и т. д. Список — `GET /v1/infra/runtimes`. Сервер создаётся из стандартного образа Ubuntu 24.04 — без runtime на нём нет ни node, ни python, ни php, и start упадёт с command not found. Альтернатива — поставить тулчейн вручную через preStart (см. ниже).
install string нет Команда установки зависимостей (до 5000 символов)
preStart string нет Команда до запуска сервиса: миграции БД, seed-скрипты, сборка, ручная установка системных пакетов (например, apt-get update && apt-get install -y nodejs npm если не передан runtime). До 5000 символов
env object нет Переменные окружения { "KEY": "value" } — будут записаны в .env и загружены systemd
systemd boolean нет true Создавать systemd-юнит для автозапуска
cleanDeploy boolean нет true Очистить extractTo перед распаковкой. Установите false для merge-деплоя
serviceName string нет app Имя systemd-юнита. Только a-z, 0-9, -. Создаёт {serviceName}.service. При смене имени между деплоями — см. «Известные особенности»
healthPath string нет / Путь для проверки работоспособности — например, /health, /api/status. Только безопасные символы
debug boolean нет false Добавить диагностический шаг debug_infols -la, sha256sum файлов, file -i для отладки шагов сборки

Поля запроса (body — multipart/form-data)

Альтернатива JSON — загрузка архива без base64. Полезно из скриптов bash/PowerShell.

Поле формы Тип Обяз. Описание
archive файл да Архив (tar.gz / zip), до 500 МБ
start string да Команда запуска
port string да Порт (строкой: "3000")
extractTo string нет По умолчанию /opt/app
runtime string нет ID рантайма
install string нет Команда установки
preStart string нет Команда до запуска
env string нет JSON-строка: {"KEY":"value"}
systemd string нет "true" / "false"
cleanDeploy string нет При multipart по умолчанию "false" (merge). Укажите "true" для чистого деплоя
serviceName string нет Имя systemd-юнита
healthPath string нет Путь для проверки работоспособности
debug string нет "true" — добавить диагностический шаг debug_info

Шаги деплоя

Шаги выполняются последовательно. Если какой-то провалился — деплой останавливается.

Шаг Условие Что делает Таймаут
stop_existing всегда systemctl stop {serviceName} — останавливает сервис с текущим именем serviceName и освобождает порт от оставшихся процессов этого же деплоя. Посторонний процесс не завершается: шаг возвращает статус warning, деплой продолжается 15с
clean cleanDeploy: true Удаляет всё в extractTo 30с
download всегда Скачивает и распаковывает архив (1 повтор при ошибке) 10 мин
cleanup_metadata после extract Удаляет macOS-сайдкары (._*, .DS_Store) — молча если архив чистый
normalize_windows_paths после extract Приводит пути с обратной косой чертой \ из zip, собранного PowerShell Compress-Archive, к обычному виду
debug_info debug: true Диагностика: ls -la, sha256sum файлов, file -i, локаль. Читает до 32 КБ в stdout
runtime runtime передан Устанавливает рантайм. При повторном деплое с тем же — переустанавливает 300с
install install передан Запускает команду установки зависимостей 300с
env env передан Пишет .env в extractTo с правами 0600
pre_start preStart передан Запускает команду до старта сервиса. Для миграций БД, seed-скриптов, сборки 300с
systemd systemd: true Создаёт {serviceName}.service и перечитывает конфигурацию systemd (systemctl daemon-reload) 30с
start всегда systemctl enable --now {serviceName} 30с
healthcheck всегда Проверка работоспособности — до 10 попыток curl localhost:{port}{healthPath} с интервалом 3 секунды, засчитывается ответ со статусом 200–399 от сервиса serviceName. Ответ 200–399 от постороннего процесса, удерживающего порт, шаг не засчитывает и завершается ошибкой ~30с
tunnel_routing всегда Проверяет, что туннель Gateway маршрутизирует на нужный порт (set_port + опрос агента). Статус warning (не error) — проверка не прошла, деплой не прерывается ~10с

Примеры

Примеры с внешним 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?stream=false" \
  -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?stream=false" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -F "archive=@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?stream=false" \
  -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?stream=false" \
  -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?stream=false" \
  -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 && python -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?stream=false`,
  {
    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?stream=false`,
  {
    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 Имя шага: stop_existing, download, runtime, install, env, pre_start, systemd, start, healthcheck, tunnel_routing, и т. д.
data.steps[].status string ok — успешно, error — провал (только последний шаг может иметь этот статус)
data.steps[].duration number Длительность шага в секундах. Присутствует не у всех шагов — только у тех, которые сами отмеряют время выполнения (runtime, install, pre_start, normalize_windows_paths, cleanup_metadata, debug_info)
data.steps[].stderr string Стандартный поток ошибок (stderr) шага при status: "error"
data.steps[].stdout string Стандартный вывод (stdout), заполняется для шага debug_info (до 32 КБ)
data.serviceName string Имя systemd-юнита (эхо параметра или "app")
data.status string "running" после успешного деплоя
data.appUrl string HTTPS-адрес приложения: https://{subdomain}.vibecode.bitrix24.tech
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. Значения — Хранилище исходников

При success: false есть дополнительно error.code, error.message, error.step с именем провалившегося шага, а data.steps содержит хронологию шагов до провала. На отдельной виртуальной машине (kind: "STANDALONE") такой ответ приходит с HTTP-статусом 200 — соединение удерживается с начала деплоя, поэтому провал шага виден по полю success, а не по статусу. У galaxy-приложения (kind: "GALAXY_APP") та же ошибка приходит со статусом 502.

Пример ответа

Успешный деплой (JSON):

JSON
{
  "success": true,
  "data": {
    "steps": [
      { "step": "stop_existing", "status": "ok" },
      { "step": "download", "status": "ok" },
      { "step": "runtime", "status": "ok", "duration": 45 },
      { "step": "install", "status": "ok", "duration": 12 },
      { "step": "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",
    "source": { "autoSaved": false, "skippedReason": "deploy from external URL" }
  }
}

Блок data.source — итог автосохранения исходников. В примерах выше деплой идёт с внешнего URL с заголовком X-Skip-Source-Snapshot, поэтому autoSaved: false, а skippedReason повторяет переданную причину. При деплое из хранилища — source.content или source.versionId — блок выглядит как { "autoSaved": true, "savedVersionId": "vN", "skippedReason": null }. Полный контракт и значения skippedReasonХранилище исходников.

SSE-поток при ?stream=true:

event: step
data: {"step":"stop_existing","status":"ok"}

event: step
data: {"step":"download","status":"ok"}

event: step
data: {"step":"runtime","status":"ok","duration":45}

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"}

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 Код Описание
400 VALIDATION_ERROR Нарушена схема запроса (отсутствует source или start, port вне диапазона 1–65535 или не передан в multipart-режиме, неверный формат архива)
400 NOT_BLACKHOLE Сервер в режиме OPEN — Deploy API недоступен
401 MISSING_API_KEY Не передан заголовок X-Api-Key
401 INVALID_API_KEY Неверный или просроченный API-ключ
402 ACCOUNT_FROZEN Баланс Вайбкод заморожен
402 BILLING_EXHAUSTED Баланс исчерпан — пополните
404 NOT_FOUND Сервер не существует, удалён или принадлежит другому API-ключу
409 EXEC_BUSY Другая операция уже выполняется на сервере. Подождите или снимите лок через `/lock`
409 SERVER_NOT_READY Сервер не готов к операции: не запущен, туннель не подключён, либо сервер числится подключённым, но у Gateway нет живого туннеля. В ответе — поле hint с причиной и следующим шагом. Платформа пытается восстановить туннель сама. Если это не удалось — разбудите сервер или вызовите `/repair` и повторите запрос
409 SNAPSHOT_REQUIRED Деплой с внешнего source.url при включённом хранилище исходников. Передайте X-Skip-Source-Snapshot: <причина> или загрузите архив в хранилище и разверните через {source: {versionId}} — см. «Известные особенности»
400 GALAXY_DEPLOY_RUNTIME_REQUIRED Деплой galaxy-приложения без runtime. Укажите рантайм, например node20
400 GALAXY_DEPLOY_CONTENT_ONLY Деплой galaxy-приложения с source.url или source.versionId. Для galaxy-приложения источник — только встроенный source.content
400 GALAXY_DEPLOY_INVALID_INSTALL Деплой galaxy-приложения с многострочной командой install. Для galaxy-приложения install — только однострочная команда, перевод строки ломает сборку
409 GALAXY_BUILD_BUSY На хосте galaxy сейчас собирается другое приложение. Повторите запрос через несколько секунд
409 GALAXY_APP_BUSY На этом galaxy-приложении уже выполняется другая операция. Повторите запрос через несколько секунд
502 GALAXY_APP_BUILD_FAILED Сборка контейнера galaxy-приложения не удалась. Хвост лога сборки — в поле buildLog
502 GALAXY_APP_START_FAILED Контейнер galaxy-приложения собрался, но упал или ушёл в перезапуск по нехватке памяти сразу после старта. Хвост лога — в поле buildLog
502 GALAXY_HOST_UNREACHABLE Хост galaxy недоступен — туннель хоста не в статусе CONNECTED. Состояние временное, повторите тот же запрос через 1–2 минуты — приложение и его данные сохранены
502 GALAXY_APP_DEPLOY_FAILED Не удалось развернуть galaxy-приложение — непредвиденный сбой деплоя. Повторите запрос
429 RATE_LIMITED Превышен лимит 10 операций в минуту на сервер
429 DEPLOY_BACKEND_BUSY Слишком много одновременных деплоев со встроенным телом запроса. Ответ содержит заголовок Retry-After: 30 — повторите через указанное время либо передайте архив ссылкой в source.url, для загрузок по ссылке ограничения нет
200 DEPLOY_FAILED Упал один из шагов процесса деплоя. Поле error.step указывает, какой. data.steps[-1].stderr содержит подробности
200 DEPLOY_TIMEOUT Шаг деплоя не уложился в отведённое ему время
200 DEPLOY_CONNECTION_TERMINATED Соединение с сервером оборвалось посреди деплоя — агент или туннель отключились. Деплой мог примениться частично. Посмотрите, на каком шаге произошёл обрыв, и повторите деплой. Если обрыв повторяется — вызовите `/repair` и повторите деплой
200 DEPLOY_TUNNEL_STALE У Gateway нет живого туннеля для сервера, хотя сервер числится подключённым. Вызовите `/repair` и повторите деплой — правка полезной нагрузки деплоя тут не поможет
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 — Ошибки.

Известные особенности

  • Статус warning на шаге tunnel_routing не означает провал деплоя. После успешного healthcheck платформа проверяет, что туннель Gateway маршрутизирует публичный трафик на нужный порт (set_port + опрос агента). Если порт ещё не определён или агент работает в режиме фиксированного порта, шаг получает статус warning (не error) и деплой не прерывается. Публичный URL приложения может около 30 секунд отдавать страницу-заглушку Black Hole, пока сканер портов агента не определит рабочий порт — после этого заглушка пропадает автоматически. Если у агента нет автоопределения порта и warning сохраняется дольше — вызовите `/repair`.

  • Код приложения передаётся через source.content, а не через start/install/preStart. Поля-команды (start, install, preStart) — это короткие shell-команды с лимитом 5000 символов каждое (command у `/exec` — 10000). Они не предназначены для содержимого файлов: base64 целого архива туда не помещается и обрезается. Сам код (tar.gz / zip, в том числе один файл) кладите в source.content (base64-строка архива, до 500 МБ на тело запроса) либо в source.url (ссылка на архив), а в start оставляйте только команду запуска (например node server.js). Для большого архива используйте source.url, чтобы не раздувать тело запроса base64'ом.

  • Три стратегии для .env. Файл пишется в {extractTo}/.env (по умолчанию /opt/app/.env) и при cleanDeploy: true удаляется перед следующим деплоем:

    1. Передавать env в каждом /deploy — перегенерируется автоматически (рекомендуется).
    2. Хранить .env вне extractTo — например, /var/lib/myapp/.env. Эта директория не трогается cleanDeploy. В start указать явный путь.
    3. cleanDeploy: false — накопительный (merge) деплой, .env переживёт, но вместе с ним останутся устаревшие файлы прошлых деплоев.
  • Автоматическая подгрузка .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.

  • Фоновые приложения тоже должны слушать порт 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 минут с момента старта прерванного деплоя. Снять лок сразу можно вызовом `DELETE /v1/infra/servers/:id/lock`. Пересоздавать сервер не нужно.

  • Долгий деплой и таймауты HTTP-клиента. Полный цикл — особенно с установкой runtime и большим npm install / pip install — реально занимает 3–15 минут. В JSON-режиме сервер удерживает соединение, отправляя HTTP 200 + chunked-пробелы каждые 15 сек до готовности (поэтому Content-Length отсутствует, а ответ выглядит «висящим»). Если ваш HTTP-клиент установит более короткий таймаут (например, curl --max-time 120 или дефолт SDK ~30–120 сек) — соединение оборвётся, но orchestrateDeploy на сервере продолжит работу. Симптомы такого обрыва: ответ обрывается на пробелах, последующие /exec и /deploy отвечают 409 EXEC_BUSY до завершения серверной части или до истечения 15-минутного лока. Что делать: (а) в curl — --max-time 900 (15 мин) или без таймаута, (б) в любом SDK — настроить таймаут запроса ≥ 900 сек для этого эндпоинта, (в) или используйте ?stream=true (SSE) — там события идут построчно по мере прохождения шагов, прокси и SDK таймауты ведут себя стабильнее. Снять застрявший лок руками — `DELETE /lock`.

  • apt-get в preStart работает без sudo. Агент и команды деплоя выполняются от имени rootsudo писать не нужно и это не вызовет ошибку.

  • Команды 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. Посторонний процесс платформа не завершает. Освободите порт командой 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. Оба формата распаковываются, но zip, собранный средством Compress-Archive из PowerShell, хранит пути с обратной косой чертой \. Вложенные файлы из такого архива могут оказаться в корне extractTo вместо своих папок, и часть кода не попадёт в нужное место. Шаг normalize_windows_paths приводит такие пути к обычному виду, но чтобы исключить проблему, соберите код в tar.gz: tar -czf app.tar.gz -C ./my-app .. Если нужен zip — упакуйте его Unix-инструментом: zip -r app.zip ..

  • Крупные inline-архивы конкурируют за слот. source.content больше ~10 КБ держит запрос в памяти на всё время деплоя, поэтому такие запросы ограничены небольшим числом одновременных (по умолчанию 2 на бэкенд). При превышении приходит 429 с подсказкой переключиться на source.url или source.versionId — у них этого лимита нет. Мелкие 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 и start платформа собирает Dockerfile с базовым образом рантайма, строкой EXPOSE <port> и командой CMD, выводимой из 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.

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