[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"docs-infra\u002Fdeploy":3,"docs-tabs-infra\u002Fdeploy":6},{"content":4,"lastmod":5},"\n# Deploy API\n\nРазвёртывание приложений на BLACKHOLE-серверах без SSH. Все операции идут через агент туннеля: команды выполняются на сервере, файлы загружаются через туннель, логи читаются из системного журнала (утилита `journalctl`). AI-агенты должны использовать эту группу эндпоинтов для полного цикла «взять код из git → установить зависимости → запустить сервис → читать логи».\n\n**Требования:** сервер в режиме `BLACKHOLE`, статус `running`, `blackholeStatus: CONNECTED`. В OPEN-режиме Deploy API вернёт `NOT_BLACKHOLE`.\n\n> **Это контракт для Black Hole VM (`kind: \"STANDALONE\"`).** Для galaxy-приложения (`kind: \"GALAXY_APP\"`) требование `CONNECTED` не действует — деплой сам собирает контейнер. Полная модель и контракт деплоя galaxy-приложения — [Galaxy-приложение](.\u002Fgalaxy.md).\n\n> **OOM galaxy-приложения → выпуск на выделенный сервер.** Если деплой galaxy-приложения падает с `502 GALAXY_APP_START_FAILED` именно по причине OOM (приложение переросло лимит памяти контейнера 512 МБ), ответ содержит структурную подсказку `error.hint` с `recoveryAction: \"graduate-to-dedicated-vm\"`. Рекомендованное действие — пересоздать приложение на отдельной виртуальной машине: [`POST \u002Fv1\u002Finfra\u002Fservers`](\u002Fdocs\u002Finfra\u002Fservers\u002Fcreate) с `placement: \"dedicated\"` и `graduateFrom` (идентификатор упавшего galaxy-приложения, который будет удалён после создания сервера), затем `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fdeploy` с тем же исходным кодом. Защита от повторного создания через заголовок `Idempotency-Key` на этот выпуск не распространяется — вместе с `graduateFrom` он вернёт `400 IDEMPOTENCY_UNSUPPORTED_WITH_GRADUATION`. Допустимо, когда политика `serverCreation` портала разрешает создание серверов и вы в пределах квоты, выделенная виртуальная машина тарифицируется (засыпает при простое). Подсказка добавляется только при причине OOM, а не при обычном краше.\n\n**Формат ответов:** все эндпоинты по умолчанию отдают **JSON** (201\u002F200 со `{\"success\": true, ...}`). Для `\u002Fdeploy` и `\u002Fexec` доступен потоковый режим через `?stream=true` — возвращается поток SSE (Server-Sent Events) с построчным прогрессом. Для AI-агентов и MCP-клиентов **всегда используйте JSON** (не добавляйте `?stream=true`) — они не умеют разбирать SSE.\n\n**Ограничения частоты:**\n\n| Ограничение | Значение |\n|-------------|----------|\n| Операций в минуту на сервер | 10 |\n| Одновременных `exec`\u002F`deploy` | 1 на сервер |\n| Таймаут `exec` | 1–600 секунд (по умолчанию 300) |\n| Размер встроенного тела (base64 в `\u002Fupload`, `\u002Fdeploy source.content`) | 500 МБ |\n| Размер файла через URL (`\u002Fupload url`, `\u002Fdeploy source.url`) | 500 МБ |\n| Размер multipart-архива в `\u002Fdeploy` | 500 МБ |\n\nСкоуп: `vibe:infra`\n\n## Операции\n\n- [Выполнить команду](.\u002Fdeploy\u002Fexec.md) — `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fexec`\n- [Загрузить файл](.\u002Fdeploy\u002Fupload.md) — `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fupload`\n- [Логи сервиса](.\u002Fdeploy\u002Flogs.md) — `GET \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flogs`\n- [Полный деплой приложения](.\u002Fdeploy\u002Fdeploy.md) — `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fdeploy`\n- [Задать порт приложения](.\u002Fdeploy\u002Fport.md) — `PATCH \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fport`\n- [Метрики туннеля](.\u002Fdeploy\u002Fmetrics.md) — `GET \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fmetrics`\n- [Снять зависший лок](.\u002Fdeploy\u002Flock.md) — `DELETE \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flock`\n- [Список рантаймов](.\u002Fdeploy\u002Fruntimes.md) — `GET \u002Fv1\u002Finfra\u002Fruntimes`\n\n## Типовой сценарий\n\n**Полный цикл деплоя в один запрос:**\n\n1. [`POST \u002Fv1\u002Finfra\u002Fservers`](\u002Fdocs\u002Finfra\u002Fservers\u002Fcreate) — создать сервер.\n2. Подождать готовности через [`GET \u002Fv1\u002Finfra\u002Fservers\u002F:id`](\u002Fdocs\u002Finfra\u002Fservers\u002Fget) (`status: \"running\"` + `blackholeStatus: \"CONNECTED\"`).\n3. [`POST \u002Fdeploy`](.\u002Fdeploy\u002Fdeploy.md) — указать `source.url` \u002F `runtime` \u002F `install` \u002F `start` \u002F `port`. Все шаги деплоя выполнятся автоматически. По умолчанию ответ JSON — `?stream=true` нужен только если хочется SSE.\n4. После деплоя — [`GET \u002Flogs?lines=100`](.\u002Fdeploy\u002Flogs.md) для проверки.\n5. При отладке — [`POST \u002Fexec`](.\u002Fdeploy\u002Fexec.md) для разовых команд.\n\n**Ручной цикл (без `\u002Fdeploy`):**\n\n1. [`POST \u002Fupload { source.url }`](.\u002Fdeploy\u002Fupload.md) — положить архив, распаковать.\n2. [`POST \u002Fexec { command: \"cd \u002Fopt\u002Fapp && npm install\" }`](.\u002Fdeploy\u002Fexec.md) — зависимости.\n3. [`POST \u002Fexec { command: \"systemctl start app\" }`](.\u002Fdeploy\u002Fexec.md) — старт сервиса.\n4. [`GET \u002Flogs?service=app&lines=50`](.\u002Fdeploy\u002Flogs.md) — подтвердить работу.\n\n[`POST \u002Fdeploy`](.\u002Fdeploy\u002Fdeploy.md) делает всё сразу и сам проверяет работоспособность приложения.\n\n## Иконка приложения\n\nИконка приложения (SVG) показывается в каталоге Bitrix24 и как фавикон во вкладке браузера. Формат, требования и порядок (фавикон-`\u003Clink>` до деплоя, загрузка `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Ficon` после) вынесены на отдельную страницу — [Иконка приложения](\u002Fdocs\u002Finfra\u002Fapp-icon).\n\n## Рецепты\n\n- [Быстрый цикл редеплоя](\u002Fdocs\u002Frecipes\u002Ffast-redeploy) — сократить цикл правок без полного прогона всех девяти шагов.\n- [Длинные операции через exec](\u002Fdocs\u002Frecipes\u002Flong-operations) — синхронный JSON, потоковый SSE или фоновый `systemd-run`.\n- [Загрузка дампа БД на сервер](\u002Fdocs\u002Frecipes\u002Fdb-dump-restore) — залить дамп и восстановить базу в фоне через `exec`.\n\n## Смотрите также\n\n- [Создание сервера](\u002Fdocs\u002Finfra\u002Fservers\u002Fcreate) — шаг до деплоя.\n- [Жизненный цикл](\u002Fdocs\u002Finfra\u002Flifecycle) — `wake` для спящего сервера перед деплоем.\n- [Переключить режим](\u002Fdocs\u002Finfra\u002Faccess\u002Fmode) — Deploy API не работает в OPEN.\n- [Корневой раздел — Инфраструктура](\u002Fdocs\u002Finfra) — лимиты и ошибки в целом.\n","2026-07-15",{"deploy\u002Fexec.md":7,"deploy\u002Fupload.md":8,"deploy\u002Flogs.md":9,"deploy\u002Fdeploy.md":10,"deploy\u002Fport.md":11,"deploy\u002Fmetrics.md":12,"deploy\u002Flock.md":13,"deploy\u002Fruntimes.md":14},"\n## Выполнить команду\n\n`POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fexec`\n\nВыполняет shell-команду на BLACKHOLE-сервере через агент туннеля. Полный стандартный вывод (`stdout`), поток ошибок (`stderr`) и код возврата передаются клиенту. По умолчанию ответ — JSON после завершения команды (рекомендуется для AI-агентов и скриптов). Если нужен построчный прогресс — передайте `?stream=true` и читайте SSE-события `stdout`\u002F`stderr`\u002F`exit`. Отказ при выполнении команды приходит в потоковом режиме событием `error`.\n\n**Для AI-агентов и MCP-клиентов обязательно используйте JSON-режим (без `?stream=true`)** — SSE они разбирать не умеют.\n\n## Параметры\n\n| Параметр | В | Тип | Обяз. | По умолч. | Описание |\n|----------|---|-----|:-----:|-----------|----------|\n| `id` | path | string (UUID) | да | — | ID BLACKHOLE-сервера, `status: running`, `blackholeStatus: CONNECTED` |\n| `stream` | query | string | нет | `false` | `true` — SSE-режим, иначе — JSON-ответ |\n\n## Поля запроса (body)\n\n| Поле | Тип | Обяз. | Описание |\n|------|-----|:-----:|----------|\n| `command` | string | **да** | Shell-команда. 1–10 000 символов |\n| `timeout` | number | нет | Таймаут выполнения в секундах: 1–600. По умолчанию 300 |\n| `workdir` | string | нет | Рабочая директория. До 500 символов |\n| `env` | object | нет | Переменные окружения: `{ \"KEY\": \"value\" }`. Только строковые значения |\n\n## Примеры\n\n### curl — личный ключ\n\n```bash\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fexec?stream=false\" \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"command\": \"ls -la \u002Fopt\u002Fapp\", \"timeout\": 30}'\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fexec?stream=false\" \\\n  -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"command\": \"npm ci --production\", \"workdir\": \"\u002Fopt\u002Fapp\", \"timeout\": 180}'\n```\n\n### JavaScript — личный ключ\n\n```javascript\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fexec?stream=false`,\n  {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': 'YOUR_API_KEY',\n      'Content-Type': 'application\u002Fjson',\n    },\n    body: JSON.stringify({\n      command: 'node -v',\n      timeout: 10,\n    }),\n  }\n)\nconst { data } = await res.json()\nconsole.log(`exit ${data.exitCode} за ${data.duration}s:\\n${data.stdout}`)\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\n\u002F\u002F Команда с переменными окружения и рабочей директорией\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fexec?stream=false`,\n  {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': 'YOUR_APP_KEY',\n      'Authorization': 'Bearer USER_SESSION_TOKEN',\n      'Content-Type': 'application\u002Fjson',\n    },\n    body: JSON.stringify({\n      command: 'npx tsx scripts\u002Fseed.ts',\n      workdir: '\u002Fopt\u002Fapp',\n      env: { DATABASE_URL: 'postgresql:\u002F\u002Flocalhost\u002Fmydb' },\n      timeout: 60,\n    }),\n  }\n)\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `success` | boolean | `true` если команда выполнилась (включая ненулевой exitCode) |\n| `data.exitCode` | number | Код возврата процесса |\n| `data.stdout` | string | Стандартный вывод команды |\n| `data.stderr` | string | Поток ошибок команды |\n| `data.duration` | number | Время выполнения в секундах |\n| `data.truncated` | boolean | `true` если `stdout`\u002F`stderr` обрезаны по размеру (5 МБ на поток) |\n\n## Пример ответа\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"exitCode\": 0,\n    \"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\u002FLinux\\n\",\n    \"stderr\": \"\",\n    \"duration\": 5,\n    \"truncated\": false\n  }\n}\n```\n\n## Пример ответа при ошибке\n\n409 — на сервере уже выполняется другая операция. Это отказ до запуска команды, поэтому HTTP-статус отражает результат:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"EXEC_BUSY\",\n    \"message\": \"Another operation is running on this server\"\n  }\n}\n```\n\n## Ошибки\n\nОшибки эндпоинта делятся на две группы, и отдаются они по-разному. Отказ до запуска команды приходит с настоящим HTTP-статусом. Отказ во время выполнения приходит в теле ответа.\n\n### Ошибки до запуска команды\n\n| HTTP | Код | Описание |\n|------|-----|----------|\n| 400 | `VALIDATION_ERROR` | Нарушена схема запроса (пустая команда, неверный таймаут, `workdir` длиннее 500 символов) |\n| 400 | `NOT_BLACKHOLE` | Сервер в режиме OPEN — Deploy API недоступен |\n| 400 | `COMMAND_TOO_LONG` | Команда длиннее 10 000 символов. В ответе `hint`: большие данные и скрипты передавайте через [`\u002Fupload`](.\u002Fupload.md), затем `bash \u002Fпуть\u002Fскрипт.sh` |\n| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |\n| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |\n| 404 | `NOT_FOUND` | Сервер не существует, удалён или принадлежит другому API-ключу |\n| 409 | `EXEC_BUSY` | На сервере уже выполняется другая `\u002Fexec` или `\u002Fdeploy`. Подождите или снимите лок через [`\u002Flock`](.\u002Flock.md). Если `EXEC_BUSY` сохраняется после снятия лока — расклиньте канал через `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Funstick` (см. ниже) |\n| 409 | `SERVER_NOT_READY` | Сервер не готов к операции: не запущен или туннель не подключён. В ответе — поле `hint` с причиной и следующим шагом. Разбудите сервер или вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите запрос. Случай «числится подключённым, но у Gateway нет живого туннеля» на этом маршруте приходит как `502 TUNNEL_NOT_FOUND` (см. ниже) |\n| 429 | `RATE_LIMITED` | Превышен лимит 10 операций в минуту на сервер |\n\n### Ошибки во время выполнения\n\n> **HTTP-статус не отражает результат команды.** На отдельной виртуальной машине (`kind: \"STANDALONE\"`) соединение удерживается на всё время выполнения, а статус `200` отправляется до его начала — поэтому и успешная, и провалившаяся команда приходят с кодом `200`. Признак отказа — поле `success: false` и объект `error` в теле ответа. **Проверяйте `success` в теле, а не HTTP-статус:** клиент, который ветвится по статусу, посчитает провалившуюся команду успешной. У galaxy-приложения (`kind: \"GALAXY_APP\"`) та же ошибка приходит со статусом `502`.\n\n| Код | Описание |\n|-----|----------|\n| `EXEC_TIMEOUT` | Превышен `timeout` (или стандартные 300 секунд). Агент завершает процесс-группу принудительно (SIGKILL, без grace-паузы). В ответе `hint` с путями решения — см. секцию «Фоновые задачи» ниже |\n| `EXEC_FAILED` | Ошибка выполнения на агенте |\n| `EXEC_BUSY` | Занят собственный exec-мьютекс агента — он уже выполняет другую команду. Отличается от платформенного лока (`409` до старта): приходит уже во время выполнения, в теле как `success: false` (на `STANDALONE` — с HTTP `200`, на `GALAXY_APP` — с `502`). В ответе `hint` с путём восстановления через [`\u002Funstick`](#расклинивание-зависшего-канала) |\n\nОтказ связи с туннелем приходит в том же виде — со своим кодом в поле `error.code` (например `TUNNEL_NOT_FOUND` или `GATEWAY_TIMEOUT: …`). Часть gateway-кодов несёт детали после двоеточия (`GATEWAY_UNREACHABLE: …`, `GATEWAY_TIMEOUT: …`), поэтому сопоставляйте `error.code` по префиксу, а не по точному совпадению. В потоковом режиме (`?stream=true`) эти же ошибки приходят SSE-событием `error`. Данные события несут `code` и `message`, поля `success` в них нет — признаком отказа служит само событие `error`.\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n- **Блокировка на уровне сервера.** Пока идёт `\u002Fexec` или `\u002Fdeploy`, второй такой вызов вернёт 409 `EXEC_BUSY`. Если предыдущий `\u002Fdeploy` был прерван (например, упал на шаге `healthcheck` или клиент оборвал соединение по таймауту), серверный лок продолжает держаться до автоматического истечения — для `\u002Fdeploy` это до 15 минут, для `\u002Fexec` — пока не истечёт `timeout` команды плюс 90 секунд. Снять лок сразу можно вызовом [`DELETE \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flock`](.\u002Flock.md), после чего `\u002Fexec` снова работает. Пересоздавать сервер не нужно.\n- **Максимальный размер `stdout`\u002F`stderr` — 5 МБ на поток.** Если вывод команды больше, приходит `truncated: true` и хвост обрезается. Команда при этом отработала, `exitCode` корректный. Для большого вывода перенаправляйте в файл: `command: \"my-cmd > \u002Fopt\u002Fapp\u002Foutput.log 2>&1\"` и потом читайте через `\u002Fexec cat \u002Fopt\u002Fapp\u002Foutput.log`.\n- **`.env` из `\u002Fdeploy` не загружается автоматически.** Systemd подгрузит `.env` при старте сервиса, но не для разовых команд через `\u002Fexec`. Если нужны `DATABASE_URL` \u002F API-ключи — передавайте в поле `env`: `{ env: { DATABASE_URL: \"...\" } }`.\n- **Два таймаута в сумме дают `timeout + 30` секунд.** Агент убивает процесс ровно по `timeout`. Gateway ждёт ещё 30 секунд на финальные события и только потом отдаёт `EXEC_TIMEOUT`.\n- **Поддержание соединения в JSON-режиме.** Если команда выполняется дольше 15 секунд, сервер периодически шлёт пробелы в тело ответа — это предотвращает таймауты nginx и промежуточных прокси. JSON-парсер игнорирует начальные и конечные пробелы, так что клиент просто прочитает корректный JSON по окончании.\n- **Клиентский таймаут: убедитесь, что он межбайтовый, а не общий.** 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` для повторных вызовов). Команды дольше нескольких минут надёжнее запускать фоновой задачей — см. «Фоновые задачи» ниже.\n\n## Расклинивание зависшего канала\n\nЕсли после [`DELETE \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flock`](.\u002Flock.md) вызов `\u002Fexec` всё ещё возвращает `EXEC_BUSY` (а `\u002Fdeploy` — падает с кодом `DEPLOY_FAILED` и сообщением «Another command is running»), значит завис exec-мьютекс на стороне агента — фоновый процесс держит канал открытым, поэтому серверный лок снят, а канал остаётся занятым. `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Funstick` снимает серверный лок И разрывает туннель агента, который при переподключении завершает зависшую команду и освобождает мьютекс. Перезагрузка сервера не выполняется.\n\nЕсли в момент вызова на сервере всё ещё выполняется легитимная операция (деплой, exec или харден), эндпоинт по умолчанию отвечает `409 OPERATION_IN_PROGRESS` и НЕ трогает её — иначе он оборвал бы работающую команду. Расклинивать нужно только зависший канал. Если вы уверены, что он действительно завис, повторите с `?force=true`.\n\nОтвет: `{ \"success\": true, \"data\": { \"backendLockReleased\": true, \"agentBounced\": true, \"reconnected\": true } }`. Для galaxy-хостов и galaxy-приложений расклинивание не поддерживается (`409 GALAXY_UNSTICK_UNSUPPORTED`).\n\n## Фоновые задачи\n\n`timeout` у `\u002Fexec` ограничен 600 секундами, а по его истечении агент завершает всю процесс-группу принудительно (SIGKILL, без grace-паузы). Операции длиннее — установка пакетов, восстановление дампа БД, тяжёлая сборка — запускайте фоновой задачей: `\u002Fexec` вернётся мгновенно, канал освободится (второй вызов не упрётся в `EXEC_BUSY`), а процесс продолжит работать независимо от HTTP-соединения.\n\n### Рекомендуемый способ — транзиентный systemd-юнит\n\n```json\n{ \"command\": \"systemctl reset-failed restore-db 2>\u002Fdev\u002Fnull; systemd-run --unit=restore-db \u002Fbin\u002Fbash \u002Fopt\u002Fdata\u002Frestore.sh\", \"timeout\": 30 }\n```\n\nЗадача получает собственный cgroup и живёт независимо от exec-сессии. Управление — стандартными вызовами:\n\n- статус: `{ \"command\": \"systemctl is-active restore-db\" }` — `active` (идёт), `inactive` (завершилась успешно), `failed` (упала).\n- логи: journald подхватывает вывод юнита автоматически — читайте через [`GET \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flogs?service=restore-db`](.\u002Flogs.md).\n- `systemctl reset-failed \u003Cимя>` перед повторным запуском — иначе systemd откажет в создании юнита с именем упавшей задачи.\n\n### Лёгкий вариант — фон с редиректом вывода\n\n```json\n{ \"command\": \"(cd \u002Fopt\u002Fapp && npm run build > \u002Ftmp\u002Fbuild.log 2>&1 &)\", \"timeout\": 10 }\n```\n\nРедирект обоих потоков в файл обязателен — он освобождает пайпы exec-сессии. Прогресс: `{ \"command\": \"tail -20 \u002Ftmp\u002Fbuild.log\" }`.\n\n### Анти-паттерн — голый `nohup`\n\n`nohup cmd &` **без редиректа** не работает: в пайпе (а не терминале) `nohup` не перенаправляет вывод, фоновый процесс наследует пайп exec-сессии, агент ждёт его закрытия до самого `timeout` — и затем принудительно завершает всю процесс-группу, включая ваш «фоновый» процесс. Всегда добавляйте `> файл 2>&1`.\n\nСекреты (пароли БД и т. п.) передавайте через поле `env`, а не внутри `command` — команды журналируются агентом (первые 200 символов). Помните также, что шаги `install`\u002F`preStart` у [`\u002Fdeploy`](.\u002Fdeploy.md) имеют собственный таймаут 300 секунд — тяжёлые установки выносите в разовый `\u002Fexec` или фоновую задачу, а не в install-скрипт.\n\n## Смотрите также\n\n- [Полный деплой](.\u002Fdeploy.md) — `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fdeploy` — цепочка нескольких `\u002Fexec` с проверкой работоспособности.\n- [Снять зависший лок](.\u002Flock.md) — `DELETE \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flock` — если `EXEC_BUSY` после обрыва.\n- [Логи сервиса](.\u002Flogs.md) — `GET \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flogs` — системный журнал через `journalctl` и мониторинг фоновых задач, запущенных через `systemd-run` (`?service=\u003Cимя юнита>`).\n- [Загрузить файл](.\u002Fupload.md) — `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fupload` — записать файл на сервер.\n- [Восстановить туннель](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) — если `SERVER_NOT_READY`.\n- [Длинные операции через exec](\u002Fdocs\u002Frecipes\u002Flong-operations) — синхронный JSON, потоковый SSE и фоновый `systemd-run` под задачи дольше 600 секунд.\n","\n## Загрузить файл\n\n`POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fupload`\n\nЗаписывает файл на BLACKHOLE-сервер через агент туннеля. Два варианта источника: встроенное base64-содержимое в теле запроса (до 500 МБ) или URL, с которого агент скачает файл сам (до 500 МБ). Поддерживается автоматическая распаковка архивов `tar.gz` \u002F `tar.bz2` \u002F `zip` — формат определяется по сигнатуре файла (magic bytes) или по расширению URL.\n\n## Параметры\n\n| Параметр | В | Тип | Обяз. | Описание |\n|----------|---|-----|:-----:|----------|\n| `id` | path | string (UUID) | да | ID BLACKHOLE-сервера, `status: running`, `blackholeStatus: CONNECTED` |\n\n## Поля запроса (body)\n\n| Поле | Тип | Обяз. | Описание |\n|------|-----|:-----:|----------|\n| `path` | string | **да** | Путь на сервере, 1–500 символов. Для архивов — куда положить загруженный архив до распаковки |\n| `content` | string | да (или `url`) | Base64-содержимое файла. Максимум 500 МБ на тело запроса |\n| `url` | string | да (или `content`) | HTTPS-URL, откуда агент скачает файл. До 500 МБ |\n| `mode` | string | нет | Права файла в восьмеричном формате: `0644`, `0755`. Применяется к файлу по пути `path` |\n| `extract` | boolean | нет | Распаковать архив после загрузки. По умолчанию `false`. Требует `content` или `url` с архивом |\n| `extractTo` | string | нет | Директория для распаковки (при `extract: true`). По умолчанию — директория из `path` |\n\nНужно передать ровно одно из: `content` или `url`.\n\n## Примеры\n\n### curl — личный ключ\n\n```bash\n# Загрузка файла по URL с автоматической распаковкой\ncurl -X POST https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fupload \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\n    \"url\": \"https:\u002F\u002Fgithub.com\u002Fuser\u002Frepo\u002Farchive\u002Fmain.tar.gz\",\n    \"path\": \"\u002Fopt\u002Fapp\u002Fsource.tar.gz\",\n    \"extract\": true,\n    \"extractTo\": \"\u002Fopt\u002Fapp\"\n  }'\n\n# Встроенное base64-содержимое — маленький файл (package.json)\nCONTENT=$(echo '{\"name\":\"my-app\",\"version\":\"1.0.0\"}' | base64)\ncurl -X POST https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fupload \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d \"{\\\"content\\\":\\\"$CONTENT\\\",\\\"path\\\":\\\"\u002Fopt\u002Fapp\u002Fpackage.json\\\",\\\"mode\\\":\\\"0644\\\"}\"\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl -X POST https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fupload \\\n  -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"url\":\"https:\u002F\u002Fexample.com\u002Fconfig.json\",\"path\":\"\u002Fetc\u002Fmyapp\u002Fconfig.json\"}'\n```\n\n### JavaScript — личный ключ\n\n```javascript\n\u002F\u002F Загрузка архива и распаковка\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fupload`,\n  {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': 'YOUR_API_KEY',\n      'Content-Type': 'application\u002Fjson',\n    },\n    body: JSON.stringify({\n      url: 'https:\u002F\u002Fgithub.com\u002Fuser\u002Fapp\u002Farchive\u002Fmain.tar.gz',\n      path: '\u002Fopt\u002Fapp\u002Fsource.tar.gz',\n      extract: true,\n      extractTo: '\u002Fopt\u002Fapp',\n    }),\n  }\n)\nconst { data } = await res.json()\nconsole.log(`Записано ${data.size} байт по пути ${data.path}${data.extracted ? ', распаковано' : ''}`)\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\n\u002F\u002F Встроенное base64-содержимое — маленький конфиг\nconst content = Buffer.from('NODE_ENV=production\\n').toString('base64')\nawait fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fupload`,\n  {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': 'YOUR_APP_KEY',\n      'Authorization': 'Bearer USER_SESSION_TOKEN',\n      'Content-Type': 'application\u002Fjson',\n    },\n    body: JSON.stringify({\n      content,\n      path: '\u002Fopt\u002Fapp\u002F.env',\n      mode: '0600',\n    }),\n  }\n)\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `success` | boolean | `true` при успешной записи |\n| `data.path` | string | Итоговый путь файла на сервере |\n| `data.size` | number | Размер в байтах |\n| `data.extracted` | boolean | `true` если архив был распакован после загрузки |\n\n## Пример ответа\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"path\": \"\u002Fopt\u002Fapp\u002Fsource.tar.gz\",\n    \"size\": 1048576,\n    \"extracted\": true\n  }\n}\n```\n\n## Пример ответа при ошибке\n\n403 — путь запрещён для загрузки:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"UPLOAD_PATH_DENIED\",\n    \"message\": \"Upload to system path is not allowed\"\n  }\n}\n```\n\n## Ошибки\n\n| HTTP | Код | Описание |\n|------|-----|----------|\n| 400 | `VALIDATION_ERROR` | Нарушена схема: одновременно `content` и `url`, либо ни одного, неверный `path`, тело больше 500 МБ |\n| 400 | `NOT_BLACKHOLE` | Сервер в режиме OPEN — Deploy API недоступен |\n| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |\n| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |\n| 403 | `UPLOAD_PATH_DENIED` | Загрузка в системные директории (`\u002Froot\u002F.ssh`, `\u002Fetc\u002Fpasswd`, `\u002Fboot`, …) запрещена |\n| 404 | `NOT_FOUND` | Сервер не существует, удалён или принадлежит другому API-ключу |\n| 409 | `SERVER_NOT_READY` | Сервер не готов к операции: не запущен или туннель не подключён. В ответе — поле `hint` с причиной и следующим шагом. Разбудите сервер или вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите запрос. Случай «числится подключённым, но у Gateway нет живого туннеля» на этом маршруте приходит как `502 TUNNEL_NOT_FOUND` (см. ниже) |\n| 429 | `RATE_LIMITED` | Превышен лимит 10 операций в минуту на сервер |\n| 429 | `DEPLOY_BACKEND_BUSY` | Слишком много одновременных загрузок с телом запроса. Платформа ограничивает число одновременных `\u002Fupload` и `\u002Fdeploy` со встроенным содержимым, чтобы не исчерпать память. Ответ несёт заголовок `Retry-After: 30` — повторите через 30 секунд. Либо передайте архив ссылкой (`url`): для загрузок по ссылке ограничения на одновременность нет |\n| 502 | `UNZIP_PREFLIGHT_FAILED` | Не удалось установить `unzip` на сервере перед распаковкой zip-архива. Чтобы не зависеть от этого шага, используйте архив `.tar.gz` |\n| 500 | код агента | Ошибка записи файла на сервере: нехватка места, права доступа и т. п. Конкретный код приходит от агента в поле `error.code` |\n| 502 | `TUNNEL_NOT_FOUND` \u002F `GATEWAY_UNREACHABLE: …` | Нет живого туннеля до сервера либо Gateway недоступен — вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите. `GATEWAY_UNREACHABLE` несёт детали после двоеточия — сопоставляйте `error.code` по префиксу |\n| 503 | `GATEWAY_TIMEOUT: …` | Gateway не ответил вовремя. Код несёт детали после двоеточия — сравнивайте по префиксу |\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n- **Автоопределение формата архива при `extract: true`.** Агент определяет формат по сигнатуре файла (magic bytes, функция `detectArchiveExt()`): поддерживает `.tar.gz`, `.zip`, `.tar.bz2`. Формат можно не указывать явно. Для URL — агент также проверяет расширение.\n- **macOS-архивы чистятся автоматически.** Если архив содержит AppleDouble-сайдкары (`._*`) или `.DS_Store`, агент удаляет их после распаковки — это предотвращает ложные срабатывания сканеров вроде Tailwind v4 oxide. При создании архива на macOS советуем использовать `COPYFILE_DISABLE=1 tar -czf ...` чтобы сайдкары вообще не попадали в архив.\n- **Windows PowerShell ZIP — работает на агенте ≥ 1.2.3.** `Compress-Archive` в Windows пишет ZIP с литеральными `\\` в именах файлов. Агент с версии 1.2.3 автоматически нормализует эти пути через шаг `normalize_windows_paths`. Для старых версий агента используйте `tar.gz` (через WSL \u002F Git Bash) либо выкладывайте готовый tar-архив и загружайте через `url`.\n- **Список запрещённых путей.** Агент блокирует загрузку в: `\u002Froot\u002F.ssh\u002F` (защита SSH-ключей), `\u002Fboot`, `\u002Fetc\u002Fshadow`, `\u002Fetc\u002Fpasswd`, системные сервис-файлы. Для приложения используйте `\u002Fopt\u002F\u003Capp>\u002F` или `\u002Fvar\u002Flib\u002F\u003Capp>\u002F`.\n- **`mode` применяется к архиву, не к распакованным файлам.** При `extract: true` права распакованных файлов определяются содержимым архива, а `mode` задаётся только сохранённому архиву. Если нужно изменить права внутри — сделайте через [`\u002Fexec`](.\u002Fexec.md) c `chmod`.\n- **Таймаут 10 минут на загрузку через `url`.** Для 500 МБ должно уложиться. Если источник медленный — предзагрузите архив, положите на свой CDN.\n\n## Смотрите также\n\n- [Полный деплой](.\u002Fdeploy.md) — за один запрос вместо цепочки `upload` + `exec`.\n- [Выполнить команду](.\u002Fexec.md) — после загрузки можно распаковать\u002Fустановить вручную.\n- [Логи сервиса](.\u002Flogs.md) — проверить, что сервис применил новые файлы.\n","\n## Логи сервиса\n\n`GET \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flogs`\n\nВозвращает логи BLACKHOLE-сервера через системную утилиту `journalctl`. Без параметра `service` читается весь системный журнал (все сервисы и общесистемные сообщения). Для логов конкретного systemd-юнита — передайте `service` (по умолчанию при деплое создаётся `app.service`, если не указали другое имя). Поддерживает фильтрацию по количеству строк, временному окну и подстроке.\n\n## Параметры\n\n| Параметр | В | Тип | Обяз. | По умолч. | Описание |\n|----------|---|-----|:-----:|-----------|----------|\n| `id` | path | string (UUID) | да | — | ID BLACKHOLE-сервера |\n| `service` | query | string | нет | — | Имя systemd-юнита: `app`, `crm-dashboard`, и т. п. Не указывайте расширение `.service`. По умолчанию читается весь системный журнал |\n| `lines` | query | number | нет | 50 | Количество строк, 1–500 |\n| `since` | query | string | нет | — | Временная метка в формате утилиты `journalctl`: `\"1 hour ago\"`, `\"2026-04-22 10:00:00\"`, `\"10 minutes ago\"` |\n| `grep` | query | string | нет | — | Подстрока для фильтра, до 200 символов |\n\n## Примеры\n\n### curl — личный ключ\n\n```bash\n# Логи приложения за последний час с фильтром по \"error\"\ncurl -H \"X-Api-Key: YOUR_API_KEY\" \\\n  \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Flogs?service=app&lines=100&since=1%20hour%20ago&grep=error\"\n\n# Весь системный журнал, последние 10 строк (для отладки cloud-init)\ncurl -H \"X-Api-Key: YOUR_API_KEY\" \\\n  \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Flogs?lines=10\"\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\" \\\n  \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Flogs?service=app&lines=50\"\n```\n\n### JavaScript — личный ключ\n\n```javascript\nconst params = new URLSearchParams({\n  service: 'app',\n  lines: '100',\n  since: '30 minutes ago',\n})\n\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Flogs?${params}`,\n  { headers: { 'X-Api-Key': 'YOUR_API_KEY' } }\n)\nconst { data } = await res.json()\ndata.logs.forEach(line => console.log(line))\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Flogs?service=app&lines=50`,\n  {\n    headers: {\n      'X-Api-Key': 'YOUR_APP_KEY',\n      'Authorization': 'Bearer USER_SESSION_TOKEN',\n    },\n  }\n)\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `success` | boolean | Всегда `true` при успехе |\n| `data.service` | string \\| null | Эхо параметра `?service=`, или `null` если не передавался |\n| `data.logs` | array\\\u003Cstring\\> | **Основное поле.** Строки логов в порядке поступления (новые — в конце массива) |\n| `data.lines` | array\\\u003Cstring\\> | Алиас `data.logs` (тот же массив). Сохранён для обратной совместимости — в новом коде используйте `data.logs` |\n| `data.requestedLines` | number | Эхо `?lines=` параметра (валидировано 1–500, default 50) |\n| `data.returnedLines` | number | Длина `data.logs` |\n| `data.since` | string \\| null | Эхо `?since=`, или `null` |\n| `data.grep` | string \\| null | Эхо `?grep=`, или `null` |\n| `data.hint` | string | Опционально. Диагностика когда `data.logs.length === 0` |\n\n## Пример ответа\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"service\": null,\n    \"logs\": [\n      \"Apr 22 11:17:02 epd65hdv07p8g89c0c06 CRON[1406]: pam_unix(cron:session): session closed for user root\",\n      \"Apr 22 11:17:27 epd65hdv07p8g89c0c06 vibe-agent[1098]: 2026\u002F04\u002F22 11:17:27 [exec] id= timeout=10s workdir=\\\"\\\" command=\\\"uname -a\\\"\",\n      \"Apr 22 11:17:28 epd65hdv07p8g89c0c06 cloud-init[975]: Reading package lists...\"\n    ],\n    \"lines\": [\n      \"Apr 22 11:17:02 epd65hdv07p8g89c0c06 CRON[1406]: pam_unix(cron:session): session closed for user root\",\n      \"Apr 22 11:17:27 epd65hdv07p8g89c0c06 vibe-agent[1098]: 2026\u002F04\u002F22 11:17:27 [exec] id= timeout=10s workdir=\\\"\\\" command=\\\"uname -a\\\"\",\n      \"Apr 22 11:17:28 epd65hdv07p8g89c0c06 cloud-init[975]: Reading package lists...\"\n    ],\n    \"requestedLines\": 50,\n    \"returnedLines\": 3,\n    \"since\": null,\n    \"grep\": null\n  }\n}\n```\n\n## Пример ответа при ошибке\n\n400 — сервер в OPEN-режиме:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"NOT_BLACKHOLE\",\n    \"message\": \"Deployment API only available for BLACKHOLE servers\"\n  }\n}\n```\n\n## Ошибки\n\n| HTTP | Код | Описание |\n|------|-----|----------|\n| 400 | `VALIDATION_ERROR` | Нарушена схема: `lines` вне 1–500, `grep` длиннее 200 символов |\n| 400 | `NOT_BLACKHOLE` | Сервер в режиме OPEN — Deploy API недоступен |\n| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |\n| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |\n| 404 | `NOT_FOUND` | Сервер не существует, удалён или принадлежит другому API-ключу |\n| 409 | `SERVER_NOT_READY` | Сервер не готов к операции: не запущен или туннель не подключён. В ответе — поле `hint` с причиной и следующим шагом. Разбудите сервер или вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите запрос. Случай «числится подключённым, но у Gateway нет живого туннеля» на этом маршруте приходит как `502 TUNNEL_NOT_FOUND` (см. ниже) |\n| 429 | `RATE_LIMITED` | Превышен лимит 10 операций в минуту на сервер |\n| 502 | `TUNNEL_NOT_FOUND` \u002F `GATEWAY_UNREACHABLE: …` | Нет живого туннеля до сервера либо Gateway недоступен — вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите. `GATEWAY_UNREACHABLE` несёт детали после двоеточия, поэтому сопоставляйте `error.code` по префиксу, а не по точному совпадению |\n| 503 | `GATEWAY_TIMEOUT: …` | Gateway не ответил вовремя. Код тоже несёт детали после двоеточия — сравнивайте по префиксу |\n| 502 | `LOGS_UNAVAILABLE` | Агент не смог прочитать системный журнал: нет такого юнита, неверный фильтр, отказано в доступе или `journalctl` вернул ошибку |\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n- **Формат строк — как выдаёт `journalctl`.** Каждая строка уже содержит время, имя хоста, имя процесса и его PID — дополнительная обработка на клиенте не нужна, выводите как есть.\n- **Фильтр `grep` — простая подстрока, не регулярное выражение.** Для сложных паттернов выгружайте больше строк и фильтруйте на клиенте.\n- **Логи агента туннеля видны без `service`** — под именем `vibe-agent`. Полезно для диагностики, когда приложение не отвечает через Deploy API. Просто не передавайте параметр `service` — увидите весь системный журнал, включая `vibe-agent`, cloud-init и другие общесистемные сообщения.\n- **Лимит 500 строк помножен на ограничение 10 вызовов в минуту** — за минуту можно прочитать максимум 5000 строк. Для большего объёма логов — несколько вызовов с разными `since` и аккуратно к лимиту.\n\n## Galaxy-приложения\n\nЕсли сервер — galaxy-приложение (`kind=GALAXY_APP`), эндпоинт возвращает не системный журнал хоста, а stdout\u002Fstderr самого контейнера приложения (`docker logs`). Параметр `service` для galaxy-приложений не применяется.\n\n- **Чтение read-only.** Запрос логов не будит спящую галактику. Если хост-галактика спит или недоступен, ответ — пустой массив `data.logs` плюс диагностическое поле `data.hint`. Разбудите галактику (любым обращением к приложению) и повторите запрос.\n- **`since` — длительность или RFC3339, не относительная текстовая форма.** Для galaxy-приложений `since` принимает только длительность в секундах\u002Fминутах\u002Fчасах (`10m`, `2h`, `24h`) или метку времени RFC3339 (`2026-06-24T10:00:00Z`) — для интервала в сутки используйте `24h`. Относительные текстовые формы `journalctl` (`\"1 hour ago\"`, `\"10 minutes ago\"`) допустимы только для Black Hole-серверов — для galaxy-приложения такое значение вернёт `400 VALIDATION_ERROR`.\n- **`grep`** работает так же — простая подстрока, фильтрация на стороне платформы.\n\n## Смотрите также\n\n- [Полный деплой](.\u002Fdeploy.md) — посмотреть, какое имя сервиса создаётся.\n- [Выполнить команду](.\u002Fexec.md) — `journalctl -u app -f` для потокового чтения логов в реальном времени через `exec`.\n- [Метрики туннеля](.\u002Fmetrics.md) — активность агента и HTTP-соединения.\n","\n## Полный деплой приложения\n\n`POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fdeploy`\n\nПолный цикл деплоя за один запрос: остановка предыдущего сервиса → скачивание архива → распаковка → установка рантайма → установка зависимостей → запись `.env` → команды `preStart` → создание systemd-юнита → запуск → проверка работоспособности. Используйте вместо цепочки `\u002Fupload` + `\u002Fexec` — быстрее и атомарно. По умолчанию ответ — JSON (рекомендуется для AI-агентов). Для построчного прогресса передайте `?stream=true` — сервер вернёт поток SSE (Server-Sent Events) с событиями на каждом шаге.\n\n**Для AI-агентов и MCP-клиентов обязательно используйте JSON-режим (без `?stream=true`).**\n\n## Galaxy-приложение — деплой сразу после создания\n\nЕсли сервер — galaxy-приложение (поле `createdVia` равно `galaxy` в ответе [`POST \u002Fv1\u002Finfra\u002Fservers`](..\u002Fservers\u002Fcreate.md)), деплой запускают **сразу после создания**, не дожидаясь `blackholeStatus: \"CONNECTED\"` — контейнер собирается самим деплоем. Модель размещения, жизненный цикл и стоимость — на странице [Galaxy-приложение](..\u002Fgalaxy.md).\n\nОтличия деплоя для galaxy-приложения:\n\n- `runtime` и `start` **обязательны**. Без `runtime` запрос вернёт `400 GALAXY_DEPLOY_RUNTIME_REQUIRED`.\n- Источник — только встроенный `source.content` (архив в base64). Варианты `source.url` и `source.versionId` для galaxy-приложения отклоняются с `400 GALAXY_DEPLOY_CONTENT_ONLY`.\n- `Dockerfile` в архиве не требуется — платформа сгенерирует его сама из `runtime`, `port` и `start`. Если положить свой `Dockerfile` в архив, он будет проигнорирован (платформа всегда генерирует свой). См. «Известные особенности» ниже.\n- Рабочая директория контейнера — **`\u002Fopt\u002Fapp`** (та же, что и `extractTo` у обычной виртуальной машины): код копируется туда, поэтому работают и относительная команда (`start: \"node server.js\"`), и абсолютный путь (`start: \"node \u002Fopt\u002Fapp\u002Fserver.js\"`). Путь `\u002Fapp` тоже ведёт на `\u002Fopt\u002Fapp` — это символьная ссылка для обратной совместимости. То есть один и тот же `start` подходит и для VM-, и для galaxy-деплоя.\n\nЖизненный цикл galaxy-приложения — на странице [Galaxy-приложение](..\u002Fgalaxy.md). Сценарий «деплой одним запросом при создании» — на странице [Создать сервер](..\u002Fservers\u002Fcreate.md).\n\n## Параметры\n\n| Параметр | В | Тип | Обяз. | По умолч. | Описание |\n|----------|---|-----|:-----:|-----------|----------|\n| `id` | path | string (UUID) | да | — | ID BLACKHOLE-сервера, `status: running`, `blackholeStatus: CONNECTED` |\n| `stream` | query | string | нет | `false` | `true` — SSE-режим, иначе — JSON-ответ |\n\n## Поля запроса (body — JSON)\n\n| Поле | Тип | Обяз. | По умолч. | Описание |\n|------|-----|:-----:|-----------|----------|\n| `source` | object | **да** | — | Источник кода — ровно один из трёх вариантов ниже (`url`, `content` или `versionId`) |\n| `source.url` | string | да (или `content` \u002F `versionId`) | — | HTTPS-URL архива с кодом. До 500 МБ |\n| `source.content` | string | да (или `url` \u002F `versionId`) | — | Base64-строка архива. До 500 МБ на тело запроса |\n| `source.versionId` | string | да (или `url` \u002F `content`) | — | ID снапшота из депо приложения, например `v3` — см. [Хранилище исходников](\u002Fdocs\u002Fsource-storage) |\n| `start` | string | **да** | — | Команда запуска приложения (пример: `cd \u002Fopt\u002Fapp && node server.js`) |\n| `port` | number | нет | `3000` | Порт, на котором приложение слушает. **Всегда 3000** для BLACKHOLE — если не передан, подставляется 3000. (В multipart-режиме порт указывать обязательно, см. таблицу ниже.) |\n| `extractTo` | string | нет | `\u002Fopt\u002Fapp` | Директория распаковки |\n| `runtime` | string | нет | — | ID рантайма: `node20`, `python311`, `php83`, `static` и т. д. Список — [`GET \u002Fv1\u002Finfra\u002Fruntimes`](.\u002Fruntimes.md). Сервер создаётся из стандартного образа Ubuntu 24.04 — без `runtime` на нём нет ни `node`, ни `python`, ни `php`, и `start` упадёт с `command not found`. Альтернатива — поставить тулчейн вручную через `preStart` (см. ниже). |\n| `install` | string | нет | — | Команда установки зависимостей (до 5000 символов) |\n| `preStart` | string | нет | — | Команда **до** запуска сервиса: миграции БД, seed-скрипты, сборка, ручная установка системных пакетов (например, `apt-get update && apt-get install -y nodejs npm` если не передан `runtime`). До 5000 символов |\n| `env` | object | нет | — | Переменные окружения `{ \"KEY\": \"value\" }` — будут записаны в `.env` и загружены systemd |\n| `systemd` | boolean | нет | `true` | Создавать systemd-юнит для автозапуска |\n| `cleanDeploy` | boolean | нет | `true` | Очистить `extractTo` перед распаковкой. Установите `false` для merge-деплоя |\n| `serviceName` | string | нет | `app` | Имя systemd-юнита. Только `a-z`, `0-9`, `-`. Создаёт `{serviceName}.service`. При смене имени между деплоями — см. «Известные особенности» |\n| `healthPath` | string | нет | `\u002F` | Путь для проверки работоспособности — например, `\u002Fhealth`, `\u002Fapi\u002Fstatus`. Только безопасные символы |\n| `debug` | boolean | нет | `false` | Добавить диагностический шаг `debug_info` — `ls -la`, `sha256sum` файлов, `file -i` для отладки шагов сборки |\n\n## Поля запроса (body — multipart\u002Fform-data)\n\nАльтернатива JSON — загрузка архива без base64. Полезно из скриптов bash\u002FPowerShell.\n\n| Поле формы | Тип | Обяз. | Описание |\n|------------|-----|:-----:|----------|\n| `archive` | файл | **да** | Архив (tar.gz \u002F zip), до 500 МБ |\n| `start` | string | **да** | Команда запуска |\n| `port` | string | **да** | Порт (строкой: `\"3000\"`) |\n| `extractTo` | string | нет | По умолчанию `\u002Fopt\u002Fapp` |\n| `runtime` | string | нет | ID рантайма |\n| `install` | string | нет | Команда установки |\n| `preStart` | string | нет | Команда до запуска |\n| `env` | string | нет | JSON-строка: `{\"KEY\":\"value\"}` |\n| `systemd` | string | нет | `\"true\"` \u002F `\"false\"` |\n| `cleanDeploy` | string | нет | **При multipart по умолчанию `\"false\"`** (merge). Укажите `\"true\"` для чистого деплоя |\n| `serviceName` | string | нет | Имя systemd-юнита |\n| `healthPath` | string | нет | Путь для проверки работоспособности |\n| `debug` | string | нет | `\"true\"` — добавить диагностический шаг `debug_info` |\n\n## Шаги деплоя\n\nШаги выполняются последовательно. Если какой-то провалился — деплой останавливается.\n\n| Шаг | Условие | Что делает | Таймаут |\n|-----|---------|------------|---------|\n| `stop_existing` | всегда | `systemctl stop {serviceName}` — останавливает сервис с текущим именем `serviceName` и освобождает порт от оставшихся процессов этого же деплоя. Посторонний процесс не завершается: шаг возвращает статус `warning`, деплой продолжается | 15с |\n| `clean` | `cleanDeploy: true` | Удаляет всё в `extractTo` | 30с |\n| `download` | всегда | Скачивает и распаковывает архив (1 повтор при ошибке) | 10 мин |\n| `cleanup_metadata` | после extract | Удаляет macOS-сайдкары (`._*`, `.DS_Store`) — молча если архив чистый | — |\n| `normalize_windows_paths` | после extract | Приводит пути с обратной косой чертой `\\` из zip, собранного PowerShell `Compress-Archive`, к обычному виду | — |\n| `debug_info` | `debug: true` | Диагностика: `ls -la`, `sha256sum` файлов, `file -i`, локаль. Читает до 32 КБ в `stdout` | — |\n| `runtime` | `runtime` передан | Устанавливает рантайм. При повторном деплое с тем же — переустанавливает | 300с |\n| `install` | `install` передан | Запускает команду установки зависимостей | 300с |\n| `env` | `env` передан | Пишет `.env` в `extractTo` с правами `0600` | — |\n| `pre_start` | `preStart` передан | Запускает команду до старта сервиса. Для миграций БД, seed-скриптов, сборки | 300с |\n| `systemd` | `systemd: true` | Создаёт `{serviceName}.service` и перечитывает конфигурацию systemd (`systemctl daemon-reload`) | 30с |\n| `start` | всегда | `systemctl enable --now {serviceName}` | 30с |\n| `healthcheck` | всегда | Проверка работоспособности — до 10 попыток `curl localhost:{port}{healthPath}` с интервалом 3 секунды, засчитывается ответ со статусом 200–399 от сервиса `serviceName`. Ответ 200–399 от постороннего процесса, удерживающего порт, шаг не засчитывает и завершается ошибкой | ~30с |\n| `tunnel_routing` | всегда | Проверяет, что туннель Gateway маршрутизирует на нужный порт (`set_port` + опрос агента). Статус `warning` (не `error`) — проверка не прошла, деплой не прерывается | ~10с |\n\n## Примеры\n\nПримеры с внешним `source.url` содержат заголовок `X-Skip-Source-Snapshot`: при включённом хранилище исходников деплой со стороннего адреса без него возвращает `409 SNAPSHOT_REQUIRED`. Подробности и альтернатива через хранилище — в разделе «Известные особенности».\n\n### curl — личный ключ\n\n```bash\n# JSON, Node.js с миграциями\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fdeploy?stream=false\" \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -H \"X-Skip-Source-Snapshot: deploy from external URL\" \\\n  -d '{\n    \"source\": { \"url\": \"https:\u002F\u002Fgithub.com\u002Fuser\u002Fapp\u002Farchive\u002Fmain.tar.gz\" },\n    \"runtime\": \"node20\",\n    \"install\": \"cd \u002Fopt\u002Fapp && npm install --production\",\n    \"preStart\": \"cd \u002Fopt\u002Fapp && npx prisma migrate deploy\",\n    \"start\": \"cd \u002Fopt\u002Fapp && node server.js\",\n    \"port\": 3000,\n    \"env\": { \"NODE_ENV\": \"production\", \"DATABASE_URL\": \"postgresql:\u002F\u002Flocalhost\u002Fmydb\" }\n  }'\n\n# Multipart — локальный архив\ntar -czf app.tar.gz -C .\u002Fmy-app .\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fdeploy?stream=false\" \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -F \"archive=@app.tar.gz\" \\\n  -F \"runtime=node20\" \\\n  -F \"install=cd \u002Fopt\u002Fapp && npm install --production\" \\\n  -F \"start=cd \u002Fopt\u002Fapp && node server.js\" \\\n  -F \"port=3000\"\n```\n\n### curl — inline-архив (минимальный набор полей)\n\n```bash\n# Канонический inline-деплой: достаточно source.content + start.\n# Тип архива определяется автоматически по сигнатуре первых байт —\n# tar.gz, zip и tar.bz2 поддерживаются, отдельное поле формата не нужно.\nB64=$(base64 \u003C app.tar.gz | tr -d '\\n')\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fdeploy?stream=false\" \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d \"{\n    \\\"source\\\": { \\\"content\\\": \\\"$B64\\\" },\n    \\\"start\\\": \\\"cd \u002Fopt\u002Fapp && node server.js\\\",\n    \\\"port\\\": 3000\n  }\"\n```\n\nОбязательны только два поля: `source` (один из `content` \u002F `url` \u002F `versionId`) и `start`. Архив можно завернуть и в zip (`zip -r app.zip .`), и в tar.gz (`tar -czf app.tar.gz -C .\u002Fmy-app .`) — платформа распознаёт оба по содержимому.\n\n### curl — произвольный стек без рантайма\n\n```bash\n# Деплой без runtime — любое ПО ставится через preStart\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fdeploy?stream=false\" \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -H \"X-Skip-Source-Snapshot: deploy from external URL\" \\\n  -d '{\n    \"source\": { \"url\": \"https:\u002F\u002Fexample.com\u002Fmy-app.tar.gz\" },\n    \"preStart\": \"apt-get update -qq && apt-get install -y ffmpeg imagemagick\",\n    \"install\": \"cd \u002Fopt\u002Fapp && npm install --production\",\n    \"start\": \"cd \u002Fopt\u002Fapp && node server.js\",\n    \"port\": 3000\n  }'\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fdeploy?stream=false\" \\\n  -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -H \"X-Skip-Source-Snapshot: deploy from external URL\" \\\n  -d '{\n    \"source\": { \"url\": \"https:\u002F\u002Fgithub.com\u002Fuser\u002Ffastapi\u002Farchive\u002Fmain.tar.gz\" },\n    \"runtime\": \"python311\",\n    \"install\": \"cd \u002Fopt\u002Fapp && pip install -r requirements.txt\",\n    \"start\": \"cd \u002Fopt\u002Fapp && python -m uvicorn main:app --host 0.0.0.0 --port 3000\",\n    \"port\": 3000\n  }'\n```\n\n### JavaScript — личный ключ\n\n```javascript\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fdeploy?stream=false`,\n  {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': 'YOUR_API_KEY',\n      'Content-Type': 'application\u002Fjson',\n      'X-Skip-Source-Snapshot': 'deploy from external URL',\n    },\n    body: JSON.stringify({\n      source: { url: 'https:\u002F\u002Fgithub.com\u002Fuser\u002Fapp\u002Farchive\u002Fmain.tar.gz' },\n      runtime: 'node20',\n      install: 'cd \u002Fopt\u002Fapp && npm install --production',\n      start: 'cd \u002Fopt\u002Fapp && node server.js',\n      port: 3000,\n    }),\n  }\n)\nconst body = await res.json()\nif (body.success) {\n  console.log(`Приложение живёт: ${body.data.appUrl}`)\n} else {\n  console.error(`Упал на шаге ${body.error.step}: ${body.error.message}`)\n}\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\nawait fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fdeploy?stream=false`,\n  {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': 'YOUR_APP_KEY',\n      'Authorization': 'Bearer USER_SESSION_TOKEN',\n      'Content-Type': 'application\u002Fjson',\n      'X-Skip-Source-Snapshot': 'deploy from external URL',\n    },\n    body: JSON.stringify({\n      source: { url: 'https:\u002F\u002Fgithub.com\u002Fuser\u002Fapp\u002Farchive\u002Fmain.tar.gz' },\n      runtime: 'node20',\n      install: 'cd \u002Fopt\u002Fapp && npm install --production',\n      start: 'cd \u002Fopt\u002Fapp && node server.js',\n      port: 3000,\n    }),\n  }\n)\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `success` | boolean | `true` — все шаги выполнены, приложение отвечает на проверку работоспособности |\n| `data.steps` | array | Массив шагов процесса деплоя, в порядке выполнения |\n| `data.steps[].step` | string | Имя шага: `stop_existing`, `download`, `runtime`, `install`, `env`, `pre_start`, `systemd`, `start`, `healthcheck`, `tunnel_routing`, и т. д. |\n| `data.steps[].status` | string | `ok` — успешно, `error` — провал (только последний шаг может иметь этот статус) |\n| `data.steps[].duration` | number | Длительность шага в секундах. Присутствует не у всех шагов — только у тех, которые сами отмеряют время выполнения (`runtime`, `install`, `pre_start`, `normalize_windows_paths`, `cleanup_metadata`, `debug_info`) |\n| `data.steps[].stderr` | string | Стандартный поток ошибок (`stderr`) шага при `status: \"error\"` |\n| `data.steps[].stdout` | string | Стандартный вывод (`stdout`), заполняется для шага `debug_info` (до 32 КБ) |\n| `data.serviceName` | string | Имя systemd-юнита (эхо параметра или `\"app\"`) |\n| `data.status` | string | `\"running\"` после успешного деплоя |\n| `data.appUrl` | string | HTTPS-адрес приложения: `https:\u002F\u002F{subdomain}.vibecode.bitrix24.tech` |\n| `data.source` | object | Итог автосохранения исходников. Только при `success: true`. В SSE-режиме приходит отдельным событием `event: source`. Полный контракт — [Хранилище исходников](\u002Fdocs\u002Fsource-storage) |\n| `data.source.autoSaved` | boolean | `true` — снимок исходников сохранён или привязан к деплою |\n| `data.source.savedVersionId` | string | Версия `vN` сохранённого снимка. Присутствует при `autoSaved: true` |\n| `data.source.skippedReason` | string \\| null | Причина, по которой снимок не сохранён, либо `null`. Значения — [Хранилище исходников](\u002Fdocs\u002Fsource-storage) |\n\nПри `success: false` есть дополнительно `error.code`, `error.message`, `error.step` с именем провалившегося шага, а `data.steps` содержит хронологию шагов до провала. На отдельной виртуальной машине (`kind: \"STANDALONE\"`) такой ответ приходит с HTTP-статусом 200 — соединение удерживается с начала деплоя, поэтому провал шага виден по полю `success`, а не по статусу. У galaxy-приложения (`kind: \"GALAXY_APP\"`) та же ошибка приходит со статусом `502`.\n\n## Пример ответа\n\nУспешный деплой (JSON):\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"steps\": [\n      { \"step\": \"stop_existing\", \"status\": \"ok\" },\n      { \"step\": \"download\", \"status\": \"ok\" },\n      { \"step\": \"runtime\", \"status\": \"ok\", \"duration\": 45 },\n      { \"step\": \"install\", \"status\": \"ok\", \"duration\": 12 },\n      { \"step\": \"env\", \"status\": \"ok\" },\n      { \"step\": \"systemd\", \"status\": \"ok\" },\n      { \"step\": \"start\", \"status\": \"ok\" },\n      { \"step\": \"healthcheck\", \"status\": \"ok\" }\n    ],\n    \"serviceName\": \"app\",\n    \"status\": \"running\",\n    \"appUrl\": \"https:\u002F\u002Fapp-abc12345.vibecode.bitrix24.tech\",\n    \"source\": { \"autoSaved\": false, \"skippedReason\": \"deploy from external URL\" }\n  }\n}\n```\n\nБлок `data.source` — итог автосохранения исходников. В примерах выше деплой идёт с внешнего URL с заголовком `X-Skip-Source-Snapshot`, поэтому `autoSaved: false`, а `skippedReason` повторяет переданную причину. При деплое из хранилища — `source.content` или `source.versionId` — блок выглядит как `{ \"autoSaved\": true, \"savedVersionId\": \"vN\", \"skippedReason\": null }`. Полный контракт и значения `skippedReason` — [Хранилище исходников](\u002Fdocs\u002Fsource-storage).\n\nSSE-поток при `?stream=true`:\n\n```\nevent: step\ndata: {\"step\":\"stop_existing\",\"status\":\"ok\"}\n\nevent: step\ndata: {\"step\":\"download\",\"status\":\"ok\"}\n\nevent: step\ndata: {\"step\":\"runtime\",\"status\":\"ok\",\"duration\":45}\n\nevent: step\ndata: {\"step\":\"install\",\"status\":\"ok\",\"duration\":12}\n\nevent: step\ndata: {\"step\":\"healthcheck\",\"status\":\"ok\"}\n\nevent: done\ndata: {\"serviceName\":\"app\",\"status\":\"running\",\"appUrl\":\"https:\u002F\u002Fapp-abc12345.vibecode.bitrix24.tech\"}\n\nevent: source\ndata: {\"autoSaved\":false,\"skippedReason\":\"deploy from external URL\"}\n```\n\n## Пример ответа при ошибке\n\nДеплой упал на шаге install (HTTP 200, `success: false`):\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"DEPLOY_FAILED\",\n    \"message\": \"npm ERR! Missing dependencies\",\n    \"step\": \"install\"\n  },\n  \"data\": {\n    \"steps\": [\n      { \"step\": \"stop_existing\", \"status\": \"ok\" },\n      { \"step\": \"download\", \"status\": \"ok\" },\n      { \"step\": \"install\", \"status\": \"error\", \"stderr\": \"npm ERR! Missing dependencies\" }\n    ]\n  }\n}\n```\n\n## Ошибки\n\n| HTTP | Код | Описание |\n|------|-----|----------|\n| 400 | `VALIDATION_ERROR` | Нарушена схема запроса (отсутствует `source` или `start`, `port` вне диапазона 1–65535 или не передан в multipart-режиме, неверный формат архива) |\n| 400 | `NOT_BLACKHOLE` | Сервер в режиме OPEN — Deploy API недоступен |\n| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |\n| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |\n| 402 | `ACCOUNT_FROZEN` | Баланс Вайбкод заморожен |\n| 402 | `BILLING_EXHAUSTED` | Баланс исчерпан — пополните |\n| 404 | `NOT_FOUND` | Сервер не существует, удалён или принадлежит другому API-ключу |\n| 409 | `EXEC_BUSY` | Другая операция уже выполняется на сервере. Подождите или снимите лок через [`\u002Flock`](.\u002Flock.md) |\n| 409 | `SERVER_NOT_READY` | Сервер не готов к операции: не запущен, туннель не подключён, либо сервер числится подключённым, но у Gateway нет живого туннеля. В ответе — поле `hint` с причиной и следующим шагом. Платформа пытается восстановить туннель сама. Если это не удалось — разбудите сервер или вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите запрос |\n| 409 | `SNAPSHOT_REQUIRED` | Деплой с внешнего `source.url` при включённом хранилище исходников. Передайте `X-Skip-Source-Snapshot: \u003Cпричина>` или загрузите архив в хранилище и разверните через `{source: {versionId}}` — см. «Известные особенности» |\n| 400 | `GALAXY_DEPLOY_RUNTIME_REQUIRED` | Деплой galaxy-приложения без `runtime`. Укажите рантайм, например `node20` |\n| 400 | `GALAXY_DEPLOY_CONTENT_ONLY` | Деплой galaxy-приложения с `source.url` или `source.versionId`. Для galaxy-приложения источник — только встроенный `source.content` |\n| 400 | `GALAXY_DEPLOY_INVALID_INSTALL` | Деплой galaxy-приложения с многострочной командой `install`. Для galaxy-приложения `install` — только однострочная команда, перевод строки ломает сборку |\n| 409 | `GALAXY_BUILD_BUSY` | На хосте galaxy сейчас собирается другое приложение. Повторите запрос через несколько секунд |\n| 409 | `GALAXY_APP_BUSY` | На этом galaxy-приложении уже выполняется другая операция. Повторите запрос через несколько секунд |\n| 502 | `GALAXY_APP_BUILD_FAILED` | Сборка контейнера galaxy-приложения не удалась. Хвост лога сборки — в поле `buildLog` |\n| 502 | `GALAXY_APP_START_FAILED` | Контейнер galaxy-приложения собрался, но упал или ушёл в перезапуск по нехватке памяти сразу после старта. Хвост лога — в поле `buildLog` |\n| 502 | `GALAXY_HOST_UNREACHABLE` | Хост galaxy недоступен — туннель хоста не в статусе `CONNECTED`. Состояние временное, повторите тот же запрос через 1–2 минуты — приложение и его данные сохранены |\n| 502 | `GALAXY_APP_DEPLOY_FAILED` | Не удалось развернуть galaxy-приложение — непредвиденный сбой деплоя. Повторите запрос |\n| 429 | `RATE_LIMITED` | Превышен лимит 10 операций в минуту на сервер |\n| 429 | `DEPLOY_BACKEND_BUSY` | Слишком много одновременных деплоев со встроенным телом запроса. Ответ содержит заголовок `Retry-After: 30` — повторите через указанное время либо передайте архив ссылкой в `source.url`, для загрузок по ссылке ограничения нет |\n| 200 | `DEPLOY_FAILED` | Упал один из шагов процесса деплоя. Поле `error.step` указывает, какой. `data.steps[-1].stderr` содержит подробности |\n| 200 | `DEPLOY_TIMEOUT` | Шаг деплоя не уложился в отведённое ему время |\n| 200 | `DEPLOY_CONNECTION_TERMINATED` | Соединение с сервером оборвалось посреди деплоя — агент или туннель отключились. Деплой мог примениться частично. Посмотрите, на каком шаге произошёл обрыв, и повторите деплой. Если обрыв повторяется — вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите деплой |\n| 200 | `DEPLOY_TUNNEL_STALE` | У Gateway нет живого туннеля для сервера, хотя сервер числится подключённым. Вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите деплой — правка полезной нагрузки деплоя тут не поможет |\n| 502 | `RUNTIME_FAILED` | Установка рантайма на сервере провалилась. Попробуйте без `runtime` или через отдельный [`\u002Fexec`](.\u002Fexec.md) |\n| 503 | `RUNTIME_TIMEOUT` | Установка рантайма не завершилась за 10 минут. Она может завершиться позже — проверьте статус сервера перед повторным запросом. Тело ответа содержит поле `retryAfter` со значением 30, ответ несёт заголовок `Retry-After: 30` — это число секунд до повторного запроса |\n\n> **HTTP-статус не отражает результат деплоя.** На отдельной виртуальной машине (`kind: \"STANDALONE\"`) соединение удерживается на всё время деплоя, а статус `200` отправляется до его начала — поэтому и успешный, и провалившийся деплой приходят с кодом `200`. Признак отказа — поле `success: false` и объект `error` в теле ответа. **Проверяйте `success` в теле, а не HTTP-статус:** клиент, который ветвится по статусу, посчитает провалившийся деплой успешным. У galaxy-приложения (`kind: \"GALAXY_APP\"`) та же ошибка приходит со статусом `502`.\n>\n> В потоковом режиме (`?stream=true`) провал шага приходит SSE-событием `step` со `status: \"error\"` — оно несёт имя провалившегося шага. При исключении или обрыве транспорта отдельное событие `error` несёт `code` и `message`. При зависшем exec-мьютексе завершающее событие `error` несёт `code: \"DEPLOY_FAILED\"`, `step` и `hint`, но без `message`. Поля `success` в потоке нет — признак отказа шага — это `status: \"error\"` в его событии `step`.\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n- **Статус `warning` на шаге `tunnel_routing` не означает провал деплоя.** После успешного `healthcheck` платформа проверяет, что туннель Gateway маршрутизирует публичный трафик на нужный порт (`set_port` + опрос агента). Если порт ещё не определён или агент работает в режиме фиксированного порта, шаг получает статус `warning` (не `error`) и деплой не прерывается. Публичный URL приложения может около 30 секунд отдавать страницу-заглушку Black Hole, пока сканер портов агента не определит рабочий порт — после этого заглушка пропадает автоматически. Если у агента нет автоопределения порта и `warning` сохраняется дольше — вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair).\n- **Код приложения передаётся через `source.content`, а не через `start`\u002F`install`\u002F`preStart`.** Поля-команды (`start`, `install`, `preStart`) — это короткие shell-команды с лимитом 5000 символов каждое (`command` у [`\u002Fexec`](.\u002Fexec.md) — 10000). Они не предназначены для содержимого файлов: base64 целого архива туда не помещается и обрезается. Сам код (tar.gz \u002F zip, в том числе один файл) кладите в `source.content` (base64-строка архива, до 500 МБ на тело запроса) либо в `source.url` (ссылка на архив), а в `start` оставляйте только команду запуска (например `node server.js`). Для большого архива используйте `source.url`, чтобы не раздувать тело запроса base64'ом.\n- **Три стратегии для `.env`.** Файл пишется в `{extractTo}\u002F.env` (по умолчанию `\u002Fopt\u002Fapp\u002F.env`) и при `cleanDeploy: true` удаляется перед следующим деплоем:\n  1. Передавать `env` в каждом `\u002Fdeploy` — перегенерируется автоматически (рекомендуется).\n  2. Хранить `.env` вне `extractTo` — например, `\u002Fvar\u002Flib\u002Fmyapp\u002F.env`. Эта директория не трогается `cleanDeploy`. В `start` указать явный путь.\n  3. `cleanDeploy: false` — накопительный (merge) деплой, `.env` переживёт, но вместе с ним останутся устаревшие файлы прошлых деплоев.\n- **Автоматическая подгрузка `.env` в systemd.** В юните прописан `EnvironmentFile=-{extractTo}\u002F.env` (знак `-` делает файл опциональным). Приложение через systemd получит переменные само. Но при `systemd: false` или командах из `install`\u002F`preStart` env подгружать не будет — передавайте явно.\n- **Если смешаны `dependencies` и `devDependencies`**, `npm install --production` не поставит пакеты из `devDependencies`. Для cron-скриптов с `tsx`, `ts-node` и т. п. уберите `--production` или добавьте их отдельно через `preStart`.\n- **Повторный деплой с тем же `runtime` переустанавливает его.** Опустите параметр `runtime` при повторных деплоях — сэкономит 45+ секунд. Первый деплой с `runtime` обязателен для установки.\n- **Проверка работоспособности может не успеть.** До 10 попыток с интервалом 3 секунды — это около 30 секунд на всё. Если приложение стартует дольше, сделайте эндпоинт `\u002Fhealth`, отвечающий 200 сразу, даже до прогрева, или переместите долгую инициализацию в `preStart`.\n- **Фоновые приложения тоже должны слушать порт 3000.** Шаг `healthcheck` выполняется при каждом деплое — даже если приложение не обслуживает HTTP-трафик (бот, который только опрашивает события, cron-демон, обработчик очереди). Без процесса, слушающего порт 3000, шаг повторяет попытки и завершается ошибкой `attempt 10\u002F10: curl exit=7` (не удалось подключиться). Поднимите рядом с основной логикой минимальный HTTP-сервер, отвечающий кодом 200 на `GET \u002F`:\n\n    Node.js:\n\n    ```javascript\n    require('http').createServer((_req, res) => res.end('ok')).listen(3000)\n    \u002F\u002F дальше — основная логика приложения\n    ```\n\n    Python:\n\n    ```python\n    import http.server, threading\n\n    class Health(http.server.BaseHTTPRequestHandler):\n        def do_GET(self):\n            self.send_response(200)\n            self.end_headers()\n            self.wfile.write(b'ok')\n        def log_message(self, *args):\n            pass\n\n    threading.Thread(\n        target=lambda: http.server.HTTPServer(('', 3000), Health).serve_forever(),\n        daemon=True,\n    ).start()\n    # дальше — основная логика приложения\n    ```\n- **Лок на 15 минут.** Один `\u002Fdeploy` блокирует `\u002Fexec` и другие `\u002Fdeploy`. Если предыдущий `\u002Fdeploy` упал на шаге `healthcheck` или клиент оборвал соединение по таймауту, последующие `\u002Fexec` и `\u002Fdeploy` будут возвращать `409 EXEC_BUSY` до автоматического истечения лока — то есть до 15 минут с момента старта прерванного деплоя. Снять лок сразу можно вызовом [`DELETE \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flock`](.\u002Flock.md). Пересоздавать сервер не нужно.\n- **Долгий деплой и таймауты HTTP-клиента.** Полный цикл — особенно с установкой `runtime` и большим `npm install` \u002F `pip install` — реально занимает 3–15 минут. В JSON-режиме сервер удерживает соединение, отправляя HTTP 200 + chunked-пробелы каждые 15 сек до готовности (поэтому `Content-Length` отсутствует, а ответ выглядит «висящим»). Если ваш HTTP-клиент установит более короткий таймаут (например, `curl --max-time 120` или дефолт SDK ~30–120 сек) — соединение оборвётся, но `orchestrateDeploy` на сервере продолжит работу. Симптомы такого обрыва: ответ обрывается на пробелах, последующие `\u002Fexec` и `\u002Fdeploy` отвечают `409 EXEC_BUSY` до завершения серверной части или до истечения 15-минутного лока. Что делать: (а) в curl — `--max-time 900` (15 мин) или без таймаута, (б) в любом SDK — настроить таймаут запроса ≥ 900 сек для этого эндпоинта, (в) или используйте `?stream=true` (SSE) — там события идут построчно по мере прохождения шагов, прокси и SDK таймауты ведут себя стабильнее. Снять застрявший лок руками — [`DELETE \u002Flock`](.\u002Flock.md).\n- **`apt-get` в `preStart` работает без `sudo`.** Агент и команды деплоя выполняются от имени `root` — `sudo` писать не нужно и это не вызовет ошибку.\n- **Команды `install`, `preStart` и `start` выполняются через POSIX-шелл `\u002Fbin\u002Fsh`.** Пишите их в POSIX-совместимом синтаксисе. Для остановки при первой же ошибке подойдёт `set -e`, для условий — `[ ... ]`. Опция `set -o pipefail` доступна только в bash — если она нужна, вызовите bash явно: `bash -c 'set -o pipefail; команды'`. То же для отдельного скрипта, запускаемого из `start`, — начните его строкой `#!\u002Fbin\u002Fbash` или запускайте через `bash script.sh`.\n- **`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 одной командой:\n  ```\n  \"preStart\": \"curl -fsSL https:\u002F\u002Fget.docker.com | sh && systemctl enable --now docker\"\n  ```\n  После этого `docker compose` (без дефиса) доступен как плагин. Если нужен только Compose V1 (устаревший, только для совместимости), он есть в Ubuntu repos: `apt-get install -y docker-compose`.\n- **`cleanDeploy: true` очищает только `extractTo`, не всю систему.** По умолчанию удаляется директория `\u002Fopt\u002Fapp` перед распаковкой нового архива. Системные пакеты (`apt`), базы данных, файлы в `\u002Fvar\u002Flib\u002F`, `\u002Fetc\u002F`, `\u002Froot\u002F` не затрагиваются — они переживут любое число повторных деплоев.\n- **Системные пакеты сохраняются при sleep\u002Fwake, reboot и repair.** Все эти операции сохраняют диск виртуальной машины нетронутым. Повторно устанавливать ПО через `preStart` при каждом деплое не нужно — достаточно сделать это однажды. Последующие деплои без `runtime` и без нужных `preStart`-команд найдут пакеты на месте.\n- **`\u002Fetc\u002Fsystemd\u002Fsystem\u002F` доступен для записи.** Можно создавать кастомные systemd-юниты через `preStart` или `\u002Fexec`: `systemctl daemon-reload && systemctl enable --now myservice`. Стандартный юнит деплоя (`app.service`) не конфликтует с пользовательскими юнитами при разных именах.\n- **При смене `serviceName` между деплоями остановите старый сервис вручную.** Шаг `stop_existing` останавливает только сервис с текущим именем `serviceName`. Если предыдущий деплой прошёл с именем по умолчанию `app`, а следующий — с именем `dedup`, то команда `systemctl stop dedup` не находит прежний `app.service`. Прежний сервис продолжает работать и удерживает порт приложения — по умолчанию 3000. Новый сервис не может занять порт и уходит в цикл перезапусков. Шаг `healthcheck` распознаёт это состояние, включая случай, когда на запрос отвечает кодом 200 прежний процесс: деплой завершается ошибкой. Сообщение об ошибке называет номер порта, состояние сервиса, процесс-держатель порта и команду для освобождения порта — `fuser -k \u003Cпорт>\u002Ftcp`. Посторонний процесс платформа не завершает. Освободите порт командой `fuser -k \u003Cпорт>\u002Ftcp`, остановите прежний сервис командой `systemctl stop \u003Cстарое-имя>.service` через [`\u002Fexec`](.\u002Fexec.md) или верните прежнее значение `serviceName`, затем повторите деплой. Оговорка: когда диагностическая проверка порта не смогла отработать, ответ со статусом 200–399 засчитывается и деплой завершается успешно.\n- **Используйте `debug: true` при расхождениях `\u002Fdeploy` и ручного пути.** Добавляет шаг `debug_info` с `sha256sum` файлов и `file -i` — помогает сравнить состояние `\u002Fdeploy` с ручным `\u002Fupload` + `\u002Fexec` в байтах. Типичный случай — Tailwind v4 oxide падает на неожиданном UTF-8.\n- **Тип архива определяется автоматически, отдельного поля формата нет.** Для `source.content` платформа читает сигнатуру первых байт (`PK` → zip, `1F 8B` → tar.gz, `BZ` → tar.bz2), для `source.url` — расширение пути. Нераспознанный архив трактуется как tar.gz. Передавать тип явным полем не нужно и негде.\n- **Для загрузки кода предпочтителен tar.gz.** Оба формата распаковываются, но zip, собранный средством `Compress-Archive` из PowerShell, хранит пути с обратной косой чертой `\\`. Вложенные файлы из такого архива могут оказаться в корне `extractTo` вместо своих папок, и часть кода не попадёт в нужное место. Шаг `normalize_windows_paths` приводит такие пути к обычному виду, но чтобы исключить проблему, соберите код в tar.gz: `tar -czf app.tar.gz -C .\u002Fmy-app .`. Если нужен zip — упакуйте его Unix-инструментом: `zip -r app.zip .`.\n- **Крупные inline-архивы конкурируют за слот.** `source.content` больше ~10 КБ держит запрос в памяти на всё время деплоя, поэтому такие запросы ограничены небольшим числом одновременных (по умолчанию 2 на бэкенд). При превышении приходит `429` с подсказкой переключиться на `source.url` или `source.versionId` — у них этого лимита нет. Мелкие inline-архивы и `versionId` слот не занимают.\n- **Деплой с внешнего URL требует снимок исходников или явный отказ.** Когда на портале включено [хранилище исходников](\u002Fdocs\u002Fsource-storage), деплой с `source.url`, который указывает не на хранилище Вайбкод, возвращает `409 SNAPSHOT_REQUIRED` — так история версий приложения не теряется при деплое со стороннего адреса. Есть два пути. Передать заголовок `X-Skip-Source-Snapshot: \u003Cпричина ≤200 символов>` — деплой продолжится без сохранения снимка. Либо сначала загрузить архив через `POST \u002Fv1\u002Fapps\u002F:id\u002Fsources` и развернуть его через `{ \"source\": { \"versionId\": \"vN\" } }` — тогда платформа сохранит версию. Источник из самого хранилища — `source.versionId` или `source.content` — этого заголовка не требует. Подробнее — [Хранилище исходников](\u002Fdocs\u002Fsource-storage).\n- **Galaxy-приложению свой `Dockerfile` не нужен — платформа генерирует его сама.** Из полей деплоя `runtime`, `port` и `start` платформа собирает `Dockerfile` с базовым образом рантайма, строкой `EXPOSE \u003Cport>` и командой `CMD`, выводимой из `start`. Класть собственный `Dockerfile` в архив не требуется и не нужно. Это касается только galaxy-приложения — поле `createdVia` равно `galaxy`.\n- **Ошибка сборки `published no host port for \u003Cport>` означает, что контейнер не удержался на `port`, а не отсутствие `EXPOSE`.** У galaxy-приложения этот текст приходит, когда контейнер упал на старте, команда `start` неверна или приложение слушает другой порт — то есть на `port` никто не отвечает. Поправьте `start` и `port` так, чтобы приложение действительно слушало `port`. Добавлять `EXPOSE` вручную не нужно: платформа уже выставила его из `port`.\n- **Переменные `env` galaxy-приложения подставляются в контейнер при запуске, а не вшиваются в образ.** Платформа передаёт их флагом `docker run --env` на старте контейнера, поэтому секреты из `env` не попадают в слои образа и не видны в его истории. Это касается только galaxy-приложения — поле `createdVia` равно `galaxy`.\n\n## Смотрите также\n\n- [Список рантаймов](.\u002Fruntimes.md) — `GET \u002Fv1\u002Finfra\u002Fruntimes` — все доступные ID.\n- [Выполнить команду](.\u002Fexec.md) — ручные команды вместо автоматической цепочки.\n- [Логи сервиса](.\u002Flogs.md) — `?service={serviceName}` для проверки старта.\n- [Снять зависший лок](.\u002Flock.md) — при `EXEC_BUSY` после обрыва.\n- [Задать порт](.\u002Fport.md) — если приложение слушает не 3000 (не рекомендуется).\n- [Хранилище исходников](\u002Fdocs\u002Fsource-storage)\n","\n## Задать порт приложения\n\n`PATCH \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fport`\n\nЗадаёт TCP-порт, на который агент туннеля проксирует входящие HTTPS-запросы с субдомена. По умолчанию туннель идёт на `:3000` — это стандарт платформы, большинство приложений должны слушать именно его. Этот эндпоинт нужен в редких случаях: приложение по историческим причинам работает на другом порту или вы хотите `0` (автоопределение), чтобы агент сам нашёл слушающий порт. Порты 1–1023 запрещены — это системные (SSH, HTTP, HTTPS, SQL), их использование создало бы риск перенаправления трафика на служебные процессы.\n\n## Параметры\n\n| Параметр | В | Тип | Обяз. | Описание |\n|----------|---|-----|:-----:|----------|\n| `id` | path | string (UUID) | да | ID BLACKHOLE-сервера, `status: running`, `blackholeStatus: CONNECTED` |\n\n## Поля запроса (body)\n\n| Поле | Тип | Обяз. | Описание |\n|------|-----|:-----:|----------|\n| `port` | number | **да** | `0` (автоопределение) или `1024–65535`. Порты 1–1023 отклоняются с `PORT_RESTRICTED` |\n\n## Примеры\n\n### curl — личный ключ\n\n```bash\n# Установить порт 8080\ncurl -X PATCH https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fport \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"port\": 8080}'\n\n# Автоматическое определение\ncurl -X PATCH https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fport \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"port\": 0}'\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl -X PATCH https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fport \\\n  -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"port\": 8080}'\n```\n\n### JavaScript — личный ключ\n\n```javascript\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fport`,\n  {\n    method: 'PATCH',\n    headers: {\n      'X-Api-Key': 'YOUR_API_KEY',\n      'Content-Type': 'application\u002Fjson',\n    },\n    body: JSON.stringify({ port: 8080 }),\n  }\n)\nconst { data } = await res.json()\nconsole.log(`Порт приложения: ${data.port}`)\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\nawait fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fport`,\n  {\n    method: 'PATCH',\n    headers: {\n      'X-Api-Key': 'YOUR_APP_KEY',\n      'Authorization': 'Bearer USER_SESSION_TOKEN',\n      'Content-Type': 'application\u002Fjson',\n    },\n    body: JSON.stringify({ port: 0 }),\n  }\n)\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `success` | boolean | Всегда `true` при успехе |\n| `data.port` | number | Итоговый порт. При `port: 0` вернётся `3000` (значение по умолчанию при автоопределении) |\n| `data.mode` | string | `manual` (задан конкретный порт) или `auto` (`port: 0`) |\n| `data.verified` | boolean | `true` — платформа подтвердила (probe), что агент уже проксирует на этот порт. `false` — порт сохранён, но маршрутизация ещё не подтверждена (см. `data.warning`) |\n| `data.warning` | string | Присутствует только при `verified: false`: маршрутизация не подтверждена, приложение может кратко (до ~30 с) отдавать страницу Black Hole, пока автоопределение не подтвердит порт |\n\n## Пример ответа\n\n```json\n{\n  \"success\": true,\n  \"data\": { \"port\": 8080, \"mode\": \"manual\", \"verified\": true }\n}\n```\n\n## Пример ответа при ошибке\n\n400 — системный порт запрещён:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"PORT_RESTRICTED\",\n    \"message\": \"System ports (1-1023) are not allowed. Use port >= 1024 or 0 for auto-detect\"\n  }\n}\n```\n\n409 — агент отклонил смену порта (фиксированный порт \u002F устаревший бинарь):\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"PORT_NOT_APPLIED\",\n    \"message\": \"The server agent runs in fixed-port mode and cannot change its port at runtime. Repair the server (POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Frepair) to switch the agent to port auto-detection, then retry.\",\n    \"agentError\": \"NO_SCANNER\"\n  }\n}\n```\n\n## Ошибки\n\n| HTTP | Код | Описание |\n|------|-----|----------|\n| 400 | `VALIDATION_ERROR` | `port` не число, вне диапазона 0–65535 |\n| 400 | `PORT_RESTRICTED` | Порт в диапазоне 1–1023 (системные порты) |\n| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |\n| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |\n| 404 | `NOT_FOUND` | Сервер не `BLACKHOLE + RUNNING`, удалён или принадлежит другому API-ключу |\n| 409 | `SERVER_NOT_READY` | Агент туннеля не в статусе `CONNECTED` |\n| 409 | `PORT_NOT_APPLIED` | Агент отклонил смену порта. `agentError: NO_SCANNER` — сервер в режиме фиксированного порта (см. ниже). Другой `agentError` — агент устарел. В обоих случаях помогает [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) |\n| 429 | `RATE_LIMITED` | Превышен общий лимит запросов платформы |\n| 502 | `GATEWAY_ERROR` | Gateway не смог доставить команду агенту |\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n- **Системные порты (1–1023) запрещены не случайно.** SSH (22), HTTP (80), HTTPS (443), DNS (53), PostgreSQL (5432), MySQL (3306) — открытие туннеля на них создало бы риск перенаправления внешнего трафика на служебные процессы внутри виртуальной машины.\n- **Как работает `port: 0`.** Агент сканирует `\u002Fproc\u002Fnet\u002Ftcp` и `\u002Fproc\u002Fnet\u002Ftcp6`, находит процесс, слушающий на прикладном порту (≥1024), и проксирует туда. Полезно для приложений, выбирающих порт динамически при запуске.\n- **Значение сохраняется между перезапусками агента.** Платформа пишет `localPort` в базу сервера — после [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) или перезагрузки агент возьмёт то же значение. При `\u002Fdeploy` с явным `port` значение перезапишется.\n- **`port: 0` в ответе возвращается как `3000`.** Это не ошибка: «при автоопределении возвращаем 3000, если ничего подходящего не нашли». Реальный слушающий порт проверяйте через [`\u002Fexec`](.\u002Fexec.md) с командой `ss -tlnp`.\n- **Платформа подтверждает смену порта (`data.verified`).** После `set_port` платформа опрашивает агента и проверяет, что туннель действительно проксирует на запрошенный порт. `verified: true` — маршрутизация готова. `verified: false` + `data.warning` — порт сохранён, но агент ещё не подтвердил порт (автоопределение сработает в течение ~30 с). Если приложение остаётся недоступным дольше — [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair). `409 PORT_NOT_APPLIED` — агент в принципе не принял смену порта: при `agentError: NO_SCANNER` сервер работает в режиме фиксированного порта (запустите приложение на текущем порту, либо `\u002Frepair` для перехода в автоопределение), при ином `agentError` — агент устарел (`\u002Frepair` обновит его).\n\n## Смотрите также\n\n- [Полный деплой](.\u002Fdeploy.md) — поле `port` в `\u002Fdeploy` также конфигурирует туннель.\n- [Метрики туннеля](.\u002Fmetrics.md) — есть ли HTTP-соединения и `lastRequestAt`.\n- [Выполнить команду](.\u002Fexec.md) — `ss -tlnp` для проверки слушающих портов.\n","\n## Метрики туннеля\n\n`GET \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fmetrics`\n\nВозвращает данные активности сервера из Black Hole Gateway в реальном времени: время подключения агента, последние HTTP и SSH запросы, количество активных соединений. Используйте для мониторинга «живо ли приложение», отладки таймаутов, проверки авто-сна. Работает только для BLACKHOLE-серверов. Если Gateway временно недоступен — ответ всё равно придёт со статусом 200, но поле `available: false`.\n\n## Параметры\n\n| Параметр | В | Тип | Обяз. | Описание |\n|----------|---|-----|:-----:|----------|\n| `id` | path | string (UUID) | да | ID BLACKHOLE-сервера |\n\n## Примеры\n\n### curl — личный ключ\n\n```bash\ncurl -H \"X-Api-Key: YOUR_API_KEY\" \\\n  https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fmetrics\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\" \\\n  https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fmetrics\n```\n\n### JavaScript — личный ключ\n\n```javascript\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fmetrics`,\n  { headers: { 'X-Api-Key': 'YOUR_API_KEY' } }\n)\nconst { data } = await res.json()\n\nif (data.available) {\n  console.log(`Подключён: ${data.connectedAt}`)\n  console.log(`Последний HTTP: ${data.lastRequestAt ?? 'никогда'}`)\n  console.log(`HTTP-соединений: ${data.httpConnections}`)\n} else {\n  console.log('Gateway недоступен, туннель может работать')\n}\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fmetrics`,\n  {\n    headers: {\n      'X-Api-Key': 'YOUR_APP_KEY',\n      'Authorization': 'Bearer USER_SESSION_TOKEN',\n    },\n  }\n)\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `success` | boolean | Всегда `true` — даже когда Gateway недоступен, эндпоинт возвращает 200 с `available: false` |\n| `data.available` | boolean | `true` — данные получены от Gateway. `false` — Gateway не отвечает (но туннель мог работать) |\n| `data.subdomain` | string | Субдомен приложения (всегда присутствует) |\n| `data.appUrl` | string | HTTPS-адрес приложения (только при `available: true`) |\n| `data.connectedAt` | string (ISO 8601) \\| null | Момент, когда агент подключился к Gateway. `null` — агент ни разу не был подключён |\n| `data.lastRequestAt` | string (ISO 8601) \\| null | Момент последнего HTTP-запроса к приложению. `null` — ни одного запроса не было |\n| `data.lastSshActivityAt` | string (ISO 8601) \\| null | Момент последней SSH-активности. Для современных BLACKHOLE-серверов всегда `null` (SSH-relay отключён) |\n| `data.httpConnections` | number | Количество активных HTTP-соединений через туннель |\n| `data.sshConnections` | number | Количество активных SSH-соединений. Всегда `0` для современных серверов |\n\n## Пример ответа\n\nGateway доступен:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"available\": true,\n    \"subdomain\": \"app-5ec02c7e\",\n    \"appUrl\": \"https:\u002F\u002Fapp-5ec02c7e.vibecode.bitrix24.tech\",\n    \"lastRequestAt\": null,\n    \"lastSshActivityAt\": null,\n    \"connectedAt\": \"2026-04-22T11:16:32Z\",\n    \"httpConnections\": 0,\n    \"sshConnections\": 0\n  }\n}\n```\n\nGateway недоступен (штатное поведение при временной недоступности):\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"available\": false,\n    \"subdomain\": \"app-5ec02c7e\"\n  }\n}\n```\n\n## Пример ответа при ошибке\n\n404 — не BLACKHOLE-сервер:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"NOT_FOUND\",\n    \"message\": \"BLACKHOLE server not found\"\n  }\n}\n```\n\n## Ошибки\n\n| HTTP | Код | Описание |\n|------|-----|----------|\n| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |\n| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |\n| 404 | `NOT_FOUND` | Сервер не в режиме BLACKHOLE, удалён или принадлежит другому API-ключу. Для OPEN-сервера эндпоинт возвращает 404 (метрики туннеля не применимы) |\n| 429 | `RATE_LIMITED` | Превышен общий лимит запросов платформы |\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n- **HTTP 200 + `available: false` — это не ошибка, а штатное поведение при временной недоступности Gateway.** Gateway временно не отвечает (перезапуск, сетевые проблемы внутри платформы), но приложение при этом **может** работать — туннель восстановится сам. В UI показывайте «метрики недоступны», а не «сервер мёртв».\n- **`connectedAt` сбрасывается при перезапуске Gateway.** Если Gateway перезапустили (редкое событие), `connectedAt` покажет момент нового подключения агента. Это не значит, что агент переподключался — Gateway просто «забыл» историю.\n- **`lastRequestAt: null` при запущенном сервере — норма.** Означает, что никто не обращался к HTTPS-субдомену приложения. Авто-сон может скоро усыпить такой сервер (если настроен через [`PATCH \u002Fsleep`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Fsleep)).\n- **`available: true` ≠ «приложение работает».** Это означает лишь, что Gateway отдал данные. Агент подключён, туннель готов — но приложение может не слушать порт 3000 и отдавать 503. Для проверки работоспособности делайте HTTP-запрос на `appUrl`.\n\n## Смотрите также\n\n- [Настроить авто-сон](\u002Fdocs\u002Finfra\u002Flifecycle\u002Fsleep) — использует `lastRequestAt`\u002F`lastSshActivityAt` для определения простоя.\n- [Получить сервер](\u002Fdocs\u002Finfra\u002Fservers\u002Fget) — `blackholeStatus: CONNECTED` подтверждает, что туннель открыт.\n- [Восстановить туннель](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) — если `connectedAt` давно и `blackholeStatus` в `DISCONNECTED`.\n- [Логи сервиса](.\u002Flogs.md) — если `available: false` сохраняется — проверить `vibe-agent` в логах.\n","\n## Снять зависший лок\n\n`DELETE \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flock`\n\nПринудительно снимает зависший лок операции на сервере. Используйте, когда [`\u002Fexec`](.\u002Fexec.md) или [`\u002Fdeploy`](.\u002Fdeploy.md) оборвались со стороны клиента (потеряно соединение, локальный таймаут), но на сервере всё ещё активен лок и последующие вызовы возвращают `EXEC_BUSY`. Эндпоинт всегда возвращает 200: если лок был — снят и в ответе `released: true`, если его не было — вернётся `released: false` (тоже успех).\n\n> **Используйте осторожно.** Снятие лока активной операции может привести к непредсказуемому состоянию сервера — одновременно запущенные `\u002Fexec` и `\u002Fdeploy` могут конкурировать за файлы и systemd-юниты.\n\n## Параметры\n\n| Параметр | В | Тип | Обяз. | Описание |\n|----------|---|-----|:-----:|----------|\n| `id` | path | string (UUID) | да | ID сервера |\n\nТело запроса пустое.\n\n## Примеры\n\n### curl — личный ключ\n\n```bash\ncurl -X DELETE -H \"X-Api-Key: YOUR_API_KEY\" \\\n  https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Flock\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl -X DELETE -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\" \\\n  https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Flock\n```\n\n### JavaScript — личный ключ\n\n```javascript\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Flock`,\n  {\n    method: 'DELETE',\n    headers: { 'X-Api-Key': 'YOUR_API_KEY' },\n  }\n)\nconst { data } = await res.json()\n\nif (data.released) {\n  console.log(`Снят лок ${data.operation}, держался ${data.ageMs}ms, до истечения было ${data.expiresInMs}ms`)\n} else {\n  console.log(data.message)  \u002F\u002F \"No active lock on this server\"\n}\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\nawait fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Flock`,\n  {\n    method: 'DELETE',\n    headers: {\n      'X-Api-Key': 'YOUR_APP_KEY',\n      'Authorization': 'Bearer USER_SESSION_TOKEN',\n    },\n  }\n)\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `success` | boolean | Всегда `true` при вызове на существующем сервере |\n| `data.released` | boolean | `true` — был активный лок, и он снят. `false` — лок отсутствовал |\n| `data.operation` | string | Тип снятой операции: `\"exec\"` или `\"deploy\"`. Присутствует только при `released: true` |\n| `data.ageMs` | number | Сколько миллисекунд лок был активен. Присутствует только при `released: true` |\n| `data.expiresInMs` | number | Через сколько миллисекунд лок истёк бы сам. Присутствует только при `released: true` |\n| `data.message` | string | Сообщение `\"No active lock on this server\"`. Присутствует только при `released: false` |\n\n## Пример ответа\n\nЛок был — снят:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"released\": true,\n    \"operation\": \"deploy\",\n    \"ageMs\": 320000,\n    \"expiresInMs\": 580000\n  }\n}\n```\n\nЛока не было:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"released\": false,\n    \"message\": \"No active lock on this server\"\n  }\n}\n```\n\n## Пример ответа при ошибке\n\n404 — сервер не найден:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"NOT_FOUND\",\n    \"message\": \"Server not found\"\n  }\n}\n```\n\n## Ошибки\n\n| HTTP | Код | Описание |\n|------|-----|----------|\n| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |\n| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |\n| 404 | `NOT_FOUND` | Сервер не существует, удалён или принадлежит другому API-ключу |\n| 429 | `RATE_LIMITED` | Превышен общий лимит запросов платформы |\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n- **Один лок на сервер.** Защита от гонок между `\u002Fexec` и `\u002Fdeploy` — они делят один общий лок. Невозможно заблокировать отдельную команду.\n- **Автосброс при нормальном завершении.** Успех или ошибка `\u002Fexec`\u002F`\u002Fdeploy` снимают лок сами. `DELETE \u002Flock` нужен только когда клиент оборвал соединение до завершения или таймаут агента прошёл без отправки финального события.\n- **TTL лока — автоистечение без вызова.** `\u002Fexec` ставит лок на `timeout + 30 + 60` секунд. `\u002Fdeploy` — на 15 минут. Даже без `DELETE \u002Flock` зависший лок истечёт сам.\n- **`released: false` — это успех**, не ошибка. Эндпоинт не возвращает 404 при отсутствии лока, потому что идея в том, чтобы «гарантировать, что лока нет». Клиенту не важно, был он или не был.\n- **Работает в любом режиме сервера.** В отличие от большинства Deploy API эндпоинтов, `DELETE \u002Flock` не требует BLACKHOLE — лок держится в памяти платформы, не зависит от агента.\n\n## Смотрите также\n\n- [Выполнить команду](.\u002Fexec.md) — основной источник `EXEC_BUSY`.\n- [Полный деплой](.\u002Fdeploy.md) — долгая операция, лок на 15 минут.\n- [Метрики туннеля](.\u002Fmetrics.md) — `available: true` + `httpConnections: 0` показывает, идёт ли реальная работа.\n","\n## Список рантаймов\n\n`GET \u002Fv1\u002Finfra\u002Fruntimes`\n\nВозвращает список ID готовых рантаймов для передачи в [`POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fdeploy`](.\u002Fdeploy.md) (параметр `runtime`). Рантайм — необязательный параметр: если нужный стек входит в список, передайте его ID и платформа установит пакеты автоматически. Если нет — деплой без `runtime` полностью рабочий. Установите нужное ПО через `preStart` в теле запроса. На [`POST \u002Fv1\u002Finfra\u002Fservers`](\u002Fdocs\u002Finfra\u002Fservers\u002Fcreate) рантайм НЕ принимается с 2026-04-25 — см. `RUNTIME_PARAM_REMOVED`. Каждый рантайм — это набор системных пакетов (nodejs, python, nginx, базы данных), устанавливаемых на сервер. Оценочное время установки — 2–4 минуты для базовых, до 4+ минут для комбинаций с несколькими пакетами.\n\n## Примеры\n\n### curl — личный ключ\n\n```bash\ncurl -H \"X-Api-Key: YOUR_API_KEY\" \\\n  https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fruntimes\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\" \\\n  https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fruntimes\n```\n\n### JavaScript — личный ключ\n\n```javascript\nconst res = await fetch('https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fruntimes', {\n  headers: { 'X-Api-Key': 'YOUR_API_KEY' },\n})\nconst { data: runtimes } = await res.json()\nruntimes.forEach(r => console.log(`${r.id}: ${r.name} (${r.estimatedSetupTime}s)`))\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\nconst res = await fetch('https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fruntimes', {\n  headers: {\n    'X-Api-Key': 'YOUR_APP_KEY',\n    'Authorization': 'Bearer USER_SESSION_TOKEN',\n  },\n})\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `success` | boolean | Всегда `true` при успехе |\n| `data` | array | Массив рантаймов |\n| `data[].id` | string | ID рантайма, передаётся как `runtime` при деплое (`POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fdeploy`) |\n| `data[].name` | string | Название для отображения пользователю |\n| `data[].packages` | array\\\u003Cstring\\> | Список пакетов, устанавливаемых на сервер (для справки) |\n| `data[].estimatedSetupTime` | number | Оценочное время установки в секундах |\n\n## Пример ответа\n\n```json\n{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"node20\",\n      \"name\": \"Node.js 20\",\n      \"packages\": [\"nodejs-20\", \"npm\", \"pm2\"],\n      \"estimatedSetupTime\": 120\n    },\n    {\n      \"id\": \"node20-pg\",\n      \"name\": \"Node.js 20 + PostgreSQL\",\n      \"packages\": [\"nodejs-20\", \"npm\", \"pm2\", \"postgresql-14\"],\n      \"estimatedSetupTime\": 180\n    },\n    {\n      \"id\": \"python311-rag\",\n      \"name\": \"Python 3.11 + RAG (pgvector + Redis)\",\n      \"packages\": [\"python3.11\", \"pip\", \"postgresql-14\", \"pgvector\", \"redis\"],\n      \"estimatedSetupTime\": 240\n    }\n  ]\n}\n```\n\n## Пример ответа при ошибке\n\n401 — не передан API-ключ:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"MISSING_API_KEY\",\n    \"message\": \"API key required. Pass via X-Api-Key header.\"\n  }\n}\n```\n\n## Ошибки\n\n| HTTP | Код | Описание |\n|------|-----|----------|\n| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |\n| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |\n| 429 | `RATE_LIMITED` | Превышен общий лимит запросов платформы |\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n- **На 2026-04-22 платформа предлагает 15 рантаймов:** шесть вариантов Node.js 20 (чистый, с PostgreSQL, MySQL, Redis, комбинации, RAG-стек), пять вариантов Python 3.11 (аналогично), два варианта PHP 8.3 (чистый и с MySQL) и `static` для SPA-статики. Актуальный снимок всегда — в ответе эндпоинта.\n- **RAG-рантаймы** (`node20-rag`, `python311-rag`) — готовая среда для схемы RAG (retrieval-augmented generation — ответ LLM с обогащением контекстом из внешних источников): PostgreSQL с расширением pgvector для векторных представлений и Redis для кэша. Сразу после установки можно подключать LlamaIndex \u002F LangChain.\n- **`static`** — рантайм для одностраничных приложений и статических сайтов: устанавливает nginx. После установки nginx запускается системным сервисом на порту 80 с конфигурацией по умолчанию и каталогом `\u002Fvar\u002Fwww\u002Fhtml` — он **не** раздаёт распакованный архив и не слушает порт 3000. Чтобы отдавать сборку из `\u002Fopt\u002Fapp` на порту 3000 с возвратом на `index.html` для клиентских маршрутов, задайте `preStart`, который создаёт серверный блок nginx и освобождает порт 80, а в `start` запустите nginx на переднем плане:\n\n    ```bash\n    # preStart: серверный блок nginx на порту 3000 + отключение системного nginx на порту 80\n    cat >\u002Fetc\u002Fnginx\u002Fconf.d\u002Fapp.conf \u003C\u003C'NGINX'\n    server {\n      listen 3000 default_server;\n      listen [::]:3000 default_server;\n      root \u002Fopt\u002Fapp;\n      index index.html;\n      location \u002F { try_files $uri $uri\u002F \u002Findex.html; }\n    }\n    NGINX\n    rm -f \u002Fetc\u002Fnginx\u002Fsites-enabled\u002Fdefault\n    systemctl disable --now nginx\n    nginx -t\n    ```\n\n    В теле деплоя: `preStart` — команда выше, `start` — `nginx -g 'daemon off;'`, `port` — `3000`. Поле `start` обязательно: платформа запускает приложение как systemd-юнит `app.service` и отдельного юнита для nginx рантайм не создаёт. Подходит для сборок React\u002FVue\u002FSvelte — положите `dist\u002F` в архив.\n- **В галактике комбинированные рантаймы НЕ поднимают базу данных.** Для galaxy-приложения (контейнер на общем хосте, а не отдельная виртуальная машина) суффиксы `-pg` \u002F `-mysql` \u002F `-redis` \u002F `-rag` носят информационный характер: устанавливается только базовый язык (Node.js \u002F Python \u002F PHP), а PostgreSQL \u002F MySQL \u002F Redis НЕ запускаются и переменные подключения (`DATABASE_URL`, `PG*` и т.п.) НЕ проставляются. Описанная выше локальная модель «база на той же машине» (`apt`-пакет `postgresql-14`, `DATABASE_URL: postgresql:\u002F\u002Flocalhost\u002F...`) относится к отдельной виртуальной машине (standalone), не к галактике. Если galaxy-приложению нужна база — используйте внешнюю (managed) БД и передавайте строку подключения через `env`, либо разверните приложение на отдельной виртуальной машине.\n- **Один и тот же ID рантайма может означать разные версии в галактике и на отдельной виртуальной машине.** В галактике рантайм собирается из официального Docker-образа (`node:20-slim`, `python:3.11-slim`, `php:8.3-cli`, `nginx:alpine`) — версия следует тегу образа. На отдельной виртуальной машине рантайм ставится `apt`-пакетом Ubuntu 24.04 — версия следует репозиторию дистрибутива и может отличаться (вплоть до major). Если версия критична — проверяйте её внутри приложения на целевой модели хоста, а не полагайтесь только на ID рантайма.\n- **`estimatedSetupTime` — оценочное, не гарантированное.** Зависит от нагрузки apt-зеркал, скорости сети виртуальной машины, конкретной сборки образа Ubuntu. Закладывайте 2× от указанного при автоматизации.\n- **Если нужного стека нет в списке — используйте `preStart`.** Сервер — чистая Ubuntu 24.04 с рут-доступом. В поле `preStart` деплоя можно выполнить любые `apt-get install`, скачать бинари, настроить окружение. Результат сохраняется между деплоями (кроме директории `extractTo`, которую очищает `cleanDeploy: true`). Для разовых операций — [`POST \u002Fexec`](.\u002Fexec.md).\n\n## Смотрите также\n\n- [Создать сервер](\u002Fdocs\u002Finfra\u002Fservers\u002Fcreate) — `POST \u002Fv1\u002Finfra\u002Fservers` (параметр `runtime` НЕ принимается с 2026-04-25).\n- [Полный деплой](.\u002Fdeploy.md) — `runtime` на этапе деплоя (единственный поддерживаемый путь).\n- [Получить сервер](\u002Fdocs\u002Finfra\u002Fservers\u002Fget) — поле `runtimeStatus`: `installing` \u002F `ready` \u002F `error`.\n"]