Выполнить команду
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 — личный ключ
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-приложение
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 — личный ключ
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-приложение
// Команда с переменными окружения и рабочей директорией
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 МБ на поток) |
Пример ответа
{
"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-статус отражает результат:
{
"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, второй такой вызов вернёт 409EXEC_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-юнит
{ "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 откажет в создании юнита с именем упавшей задачи.
Лёгкий вариант — фон с редиректом вывода
{ "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— цепочка нескольких/execс проверкой работоспособности. - Снять зависший лок —
DELETE /v1/infra/servers/:id/lock— еслиEXEC_BUSYпосле обрыва. - Логи сервиса —
GET /v1/infra/servers/:id/logs— системный журнал черезjournalctlи мониторинг фоновых задач, запущенных черезsystemd-run(?service=<имя юнита>). - Загрузить файл —
POST /v1/infra/servers/:id/upload— записать файл на сервер. - Восстановить туннель — если
SERVER_NOT_READY. - Длинные операции через exec — синхронный JSON, потоковый SSE и фоновый
systemd-runпод задачи дольше 600 секунд.
Полный деплой приложения
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_info — ls -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 — личный ключ
# 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-архив (минимальный набор полей)
# Канонический 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 — произвольный стек без рантайма
# Деплой без 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-приложение
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 — личный ключ
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-приложение
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):
{
"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):
{
"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удаляется перед следующим деплоем:- Передавать
envв каждом/deploy— перегенерируется автоматически (рекомендуется). - Хранить
.envвнеextractTo— например,/var/lib/myapp/.env. Эта директория не трогаетсяcleanDeploy. Вstartуказать явный путь. cleanDeploy: false— накопительный (merge) деплой,.envпереживёт, но вместе с ним останутся устаревшие файлы прошлых деплоев.
- Передавать
Автоматическая подгрузка
.envв systemd. В юните прописанEnvironmentFile=-{extractTo}/.env(знак-делает файл опциональным). Приложение через systemd получит переменные само. Но приsystemd: falseили командах изinstall/preStartenv подгружать не будет — передавайте явно.Если смешаны
dependenciesиdevDependencies,npm install --productionне поставит пакеты изdevDependencies. Для cron-скриптов сtsx,ts-nodeи т. п. уберите--productionили добавьте их отдельно черезpreStart.Повторный деплой с тем же
runtimeпереустанавливает его. Опустите параметрruntimeпри повторных деплоях — сэкономит 45+ секунд. Первый деплой сruntimeобязателен для установки.Проверка работоспособности может не успеть. До 10 попыток с интервалом 3 секунды — это около 30 секунд на всё. Если приложение стартует дольше, сделайте эндпоинт
/health, отвечающий 200 сразу, даже до прогрева, или переместите долгую инициализацию вpreStart.Фоновые приложения тоже должны слушать порт 3000. Шаг
healthcheckвыполняется при каждом деплое — даже если приложение не обслуживает HTTP-трафик (бот, который только опрашивает события, cron-демон, обработчик очереди). Без процесса, слушающего порт 3000, шаг повторяет попытки и завершается ошибкойattempt 10/10: curl exit=7(не удалось подключиться). Поднимите рядом с основной логикой минимальный HTTP-сервер, отвечающий кодом 200 наGET /:Node.js:
require('http').createServer((_req, res) => res.end('ok')).listen(3000) // дальше — основная логика приложенияPython:
import http.server, threading class Health(http.server.BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.end_headers() self.wfile.write(b'ok') def log_message(self, *args): pass threading.Thread( target=lambda: http.server.HTTPServer(('', 3000), Health).serve_forever(), daemon=True, ).start() # дальше — основная логика приложенияЛок на 15 минут. Один
/deployблокирует/execи другие/deploy. Если предыдущий/deployупал на шагеhealthcheckили клиент оборвал соединение по таймауту, последующие/execи/deployбудут возвращать409 EXEC_BUSYдо автоматического истечения лока — то есть до 15 минут с момента старта прерванного деплоя. Снять лок сразу можно вызовом `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. Агент и команды деплоя выполняются от имениroot—sudoписать не нужно и это не вызовет ошибку.Команды
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.Переменные
envgalaxy-приложения подставляются в контейнер при запуске, а не вшиваются в образ. Платформа передаёт их флагомdocker run --envна старте контейнера, поэтому секреты изenvне попадают в слои образа и не видны в его истории. Это касается только galaxy-приложения — полеcreatedViaравноgalaxy.
Смотрите также
- Список рантаймов —
GET /v1/infra/runtimes— все доступные ID. - Выполнить команду — ручные команды вместо автоматической цепочки.
- Логи сервиса —
?service={serviceName}для проверки старта. - Снять зависший лок — при
EXEC_BUSYпосле обрыва. - Задать порт — если приложение слушает не 3000 (не рекомендуется).
- Хранилище исходников