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

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

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

**Для AI-агентов и MCP-клиентов обязательно используйте JSON-режим (без `?stream=true`)** — SSE они разбирать не умеют. Официальный MCP-клиент платформы Вайбкод (инструмент `manage_server_deploy`) снимает этот параметр сам и всегда работает в JSON-режиме.

Потоковый режим даёт живой прогресс, но **не увеличивает предельное время выполнения**: оно одинаково в обоих режимах и равно `timeout + 30` секунд, а сам `timeout` ограничен 600 секундами. Для работы, которая заведомо в это не укладывается, запускайте фоновую задачу — см. «Фоновые задачи» ниже.

## На каких серверах работает

Тип сервера приходит в поле `kind` ответов [`GET /v1/infra/servers`](/docs/infra/servers/list) и [`GET /v1/infra/servers/:id`](/docs/infra/servers/get).

| `kind` | Команда выполняется | Что учесть |
|--------|---------------------|------------|
| `STANDALONE` — отдельная виртуальная машина | да | Контракт страницы действует целиком, включая `workdir` и `env` |
| `GALAXY_APP` — приложение в галактике | да, внутри контейнера приложения | Поля `workdir` и `env` не поддерживаются — `400 GALAXY_EXEC_NO_WORKDIR_ENV`. Вместо них добавьте приставку к самой команде: `cd /opt/app; FOO=bar node script.js`. Ошибки во время выполнения приходят со статусом `502`, а не `200` — кроме `EXEC_BUSY`, который приходит как `409` (см. ниже) |
| `GALAXY` — машина, несущая контейнеры приложений | нет — `403 GALAXY_HOST_EXEC_FORBIDDEN` | На машине стоят контейнеры разных ключей одного аккаунта Битрикс24, поэтому целью команд она не является. Выполняйте команду на приложении, по его собственному id (`kind` равен `GALAXY_APP`). Свободное место на машине смотрите в кабинете, на карточке галактики — см. [Galaxy-приложение](/docs/infra/galaxy) |

## Параметры

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

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

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

## Примеры

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

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

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

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

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

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

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

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

## Поля ответа

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

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

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

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

409 — на сервере уже выполняется другая операция. Это отказ до запуска команды, поэтому HTTP-статус отражает результат. С этим же статусом у galaxy-приложения приходит занятость общего exec-канала хоста — она возникает уже во время выполнения, но по смыслу это тоже «занято, повторите»:

```json
{
  "success": false,
  "error": {
    "code": "EXEC_BUSY",
    "message": "Another operation is running on this server",
    "retryable": true,
    "retryAfter": 10
  }
}
```

Ответ также несёт HTTP-заголовок `Retry-After` (в секундах, совпадает с полем `retryAfter`) — это короткий интервал опроса: повторяйте вызов с ним, пока блокировка не снимется. Полный верхний предел «когда лок снимется сам» лежит в `error.hint.autoExpiresInSeconds`.

## Ошибки

Ошибки эндпоинта делятся на две группы, и отдаются они по-разному. Отказ до запуска команды приходит с настоящим HTTP-статусом. Отказ во время выполнения приходит в теле ответа — с одним исключением: занятость exec-канала (`EXEC_BUSY`) у galaxy-приложения приходит настоящим `409`, потому что это отказ «занято», а не сбой.

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

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `VALIDATION_ERROR` | Нарушена схема запроса (пустая команда, неверный таймаут, `workdir` длиннее 500 символов) |
| 400 | `NOT_BLACKHOLE` | Сервер в режиме OPEN — Deploy API недоступен |
| 400 | `COMMAND_TOO_LONG` | Команда длиннее 10 000 символов. В ответе `hint`: большие данные и скрипты передавайте через [`/upload`](./upload.md), затем `bash /путь/скрипт.sh` |
| 400 | `GALAXY_EXEC_NO_WORKDIR_ENV` | В команде для galaxy-приложения (`kind: "GALAXY_APP"`) переданы `workdir` или `env`. Контейнерный запуск их не принимает — добавьте приставку к самой команде: `cd /opt/app; FOO=bar node script.js` |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |
| 402 | `ACCOUNT_FROZEN` | Баланс Вайбкод заморожен. Запрос отбивается до операции, спящий сервер при этом не будится — пополните баланс и повторите |
| 403 | `WRONG_KEY` | Сервер существует, но вызывающему не принадлежит. Операции над содержимым приложения — выкладка, команда, загрузка файла и чтение логов — открыты по любому из трёх оснований: управляющий ключ сервера, ключ, приложение которого привязано к этому серверу (`Application.serverId`), либо членство в команде разработки этого сервера (обе роли, «Разработчик» и «Администратор»). Токены доступа и загрузка значка требуют именно управляющего ключа. Управление самой машиной открыто ещё и роли «Администратор» команды. В ответе — `hint` с восстановлением из двух шагов. Подробнее — [Восстановление доступа к серверу](/docs/infra/server-access-recovery) |
| 403 | `SERVER_WAKE_BLOCKED` | Сервер спит, а пробуждение запрещено платформой — завершённый пробный период или административная блокировка. Команда такой сервер не поднимает, повтор не поможет, пока запрет не снят. Разбор запрета — [Разбудить сервер](/docs/infra/lifecycle/wake) |
| 403 | `GALAXY_HOST_EXEC_FORBIDDEN` | Команда пришла на id машины-галактики (`kind: "GALAXY"`). Выполняйте её на приложении, по его собственному id — см. «На каких серверах работает» выше |
| 403 | `INFRA_FORBIDDEN_FOR_COWORK_KEY` | Вызов сделан ключом Cowork/Code — такой ключ работает только с данными, изменяющие операции ему закрыты. Что делать — [Проектный ключ для деплоя](/docs/cowork/deploy-key) |
| 404 | `NOT_FOUND` | Сервера с таким `id` нет или он удалён. Сервер, который существует, но вам не принадлежит, отвечает `403 WRONG_KEY` — код различает «нет такого сервера» и «нет прав на него» |
| 409 | `EXEC_BUSY` | На сервере уже выполняется другая `/exec` или `/deploy`. Ответ несёт заголовок `Retry-After` и поля `retryable: true` / `retryAfter` (секунды) — повторяйте с этим интервалом. Либо снимите лок через [`/lock`](./lock.md). Если `EXEC_BUSY` сохраняется после снятия лока — освободите канал принудительно через [`POST /v1/infra/servers/:id/unstick`](/docs/infra/servers/unstick). У galaxy-хоста и galaxy-приложения принудительное освобождение владельцу недоступно (`409 GALAXY_UNSTICK_UNSUPPORTED`): exec-канал общий для всех приложений хоста — повторите через 30-60 с, при устойчивом отказе обратитесь в поддержку |
| 409 | `SERVER_NOT_READY` | Сервер не готов к операции: не запущен или туннель не подключён. В ответе — поле `hint` с причиной и следующим шагом. Спящий сервер этот код не описывает — его платформа будит сама. Запустите остановленный сервер вызовом [`/start`](/docs/infra/lifecycle/start) либо восстановите туннель вызовом [`/repair`](/docs/infra/lifecycle/repair) и повторите запрос. Случай «числится подключённым, но у Gateway нет живого туннеля» на этом маршруте приходит как `502 TUNNEL_NOT_FOUND` (см. ниже) |
| 409 | `WAKE_IN_PROGRESS` | Спящий сервер уже будит другой запрос. Дождитесь его завершения и повторите |
| 422 | `VM_MISSING` | У записи сервера нет виртуальной машины у облачного провайдера, поэтому будить нечего. Удалите сервер и создайте новый |
| 502 | `WAKE_FAILED` | Во время пробуждения спящего сервера он перешёл в неожиданное состояние. Повторите запрос |
| 502 | `PROVIDER_ERROR` | Облачный провайдер вернул ошибку при запуске виртуальной машины спящего сервера. Туннель здесь ни при чём — вызов [`/repair`](/docs/infra/lifecycle/repair) не поможет |
| 409 | `GALAXY_APP_NOT_READY` | Только у galaxy-приложения (`kind: "GALAXY_APP"`): платформа не определила имя его контейнера на хосте и команду не запустила. Два условия. Имя контейнера не прошло проверку — в `message` приходит `invalid on-host name`. Либо у приложения нет субдомена или связи с galaxy-хостом — тогда в `message` приходит `Galaxy app is missing its subdomain or host link`. Текущее состояние приложения — [`GET /v1/infra/servers/:id`](/docs/infra/servers/get) |
| 429 | `RATE_LIMITED` | Превышен лимит 10 операций в минуту на сервер |
| 503 | `WAKE_TIMEOUT` | Спящий сервер не поднялся за отведённые 6.5 минуты — машина не запустилась или туннель не подключился. Сервер возвращается в `sleeping`, поэтому повторный запрос безопасен. См. «Известные особенности» |

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

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

| Код | Описание |
|-----|----------|
| `EXEC_TIMEOUT` | Превышен `timeout` (или стандартные 300 секунд). Агент завершает процесс-группу принудительно (SIGKILL, без grace-паузы). В ответе — объект `hint`, его состав разобран ниже |
| `EXEC_FAILED` | Ошибка выполнения на агенте |
| `EXEC_NO_EXIT` | Поток завершился, не прислав статус выхода. Что успела сделать команда на сервере — неизвестно, поэтому это не успех: агент мог перестать отвечать, туннель — оборваться в середине команды, а очень большой вывод — переполнить канал уже после того, как команда отработала. В `data` возвращается накопленный к моменту обрыва вывод (`stdout`, `stderr`). Полей `exitCode`, `duration` и `truncated` нет — их значения платформе неизвестны. В ответе `hint` с путём восстановления. Только на отдельной виртуальной машине (`kind: "STANDALONE"`). У galaxy-приложения этот случай пока приходит как `success: true` с `exitCode: -1` |
| `EXEC_BUSY` | Занят собственный exec-мьютекс агента — он уже выполняет другую команду. Отличается от платформенного лока (`409` до старта): приходит уже во время выполнения, в теле как `success: false` (на `STANDALONE` — с HTTP `200`, на `GALAXY_APP` — с `409`, вместе с заголовком `Retry-After` и полями `retryable: true` / `retryAfter`). В ответе `hint` с путём восстановления: на `STANDALONE` — через [`/unstick`](/docs/infra/servers/unstick), на `GALAXY_APP` — повтор и обращение в поддержку, потому что общий канал хоста владелец приложения освободить не может |
| `CONTAINER_NOT_READY` | Хост галактики ответил, но контейнера приложения, в котором должна была выполниться команда, на нём ещё нет: слот заведён и ни разу не выложен, выкладка идёт прямо сейчас или контейнер перезапускается. Приходит со статусом `502`. В ответе `hint`, который ведёт к выкладке приложения, а не к хосту — хост исправен, будить и чинить его незачем. Только у galaxy-приложения (`kind: "GALAXY_APP"`) |

#### Состав `hint` у `EXEC_TIMEOUT`

| Поле | Тип | Описание |
|------|-----|----------|
| `hint.reason` | string | Что произошло: команда не уложилась в отведённое время, и вся процесс-группа завершена немедленно, без grace-паузы |
| `hint.recovery` | string | Что делать: верхняя граница `timeout` — 600 секунд, для более длинных операций запускайте фоновую задачу (`systemd-run --unit=<name>`) и следите за ней через [`GET /v1/infra/servers/:id/logs`](./logs.md) с параметром `?service=<имя>`, а для прогресса в пределах таймаута передайте `?stream=true` |
| `hint.recoveryAction` | string | Указатель на документацию: `docs: /docs/infra/deploy/exec` |

Подсказка приходит и в JSON-ответе, и в SSE-событии `error` потокового режима. Такой же по составу объект `hint` несёт агентский `EXEC_BUSY` — на отдельной виртуальной машине (`kind: "STANDALONE"`) его `recoveryAction` указывает на `POST /v1/infra/servers/:id/unstick`. У galaxy-приложения этот путь владельцу закрыт, поэтому там `recoveryAction` описывает повтор и обращение в поддержку.

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

Полный список общих ошибок API — [Ошибки](/docs/errors).

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

- **Спящую отдельную виртуальную машину команда будит сама.** Если сервер (`kind: "STANDALONE"`) в статусе `sleeping`, платформа запускает пробуждение и ждёт его в том же запросе — до 6.5 минут, и только потом выполняет команду. Отдельный вызов [`/wake`](/docs/infra/lifecycle/wake) не нужен, но клиентский таймаут запроса это время обязан пережить. Если сервер не поднялся, приходит `503 WAKE_TIMEOUT` и сервер возвращается в `sleeping` — команда не выполнялась, повторный запрос безопасен.
- **Команда исполняется через `/bin/sh`.** Поле `command` запускается интерпретатором `/bin/sh` — это минимальный POSIX-шелл (`dash`), не `bash`. Конструкции, специфичные для `bash` (`set -o pipefail`, `[[ … ]]`, массивы), в нём недоступны и завершаются ошибкой. Запускайте их явно — через `command: "bash -c 'set -o pipefail; …'"` либо файлом-скриптом `command: "/bin/bash /opt/app/script.sh"`.
- **Блокировка на уровне сервера.** Пока идёт `/exec` или `/deploy`, второй такой вызов вернёт 409 `EXEC_BUSY`. Если предыдущий `/deploy` был прерван (например, упал на шаге `healthcheck` или клиент оборвал соединение по таймауту), серверный лок продолжает держаться до автоматического истечения — для `/deploy` это до 15 минут, для `/exec` — пока не истечёт `timeout` команды плюс 90 секунд. Снять лок сразу можно вызовом [`DELETE /v1/infra/servers/:id/lock`](./lock.md), после чего `/exec` снова работает. Пересоздавать сервер не нужно.
- **Максимальный размер `stdout`/`stderr` — 5 МБ на поток.** Если вывод команды больше, приходит `truncated: true` и хвост обрезается. Команда при этом отработала, `exitCode` корректный. Для большого вывода перенаправляйте в файл: `command: "my-cmd > /opt/app/output.log 2>&1"` и потом читайте через `/exec cat /opt/app/output.log`.
- **`.env` из `/deploy` не загружается автоматически.** Systemd подгрузит `.env` при старте сервиса, но не для разовых команд через `/exec`. Если нужны `DATABASE_URL` / API-ключи — передавайте в поле `env`: `{ env: { DATABASE_URL: "..." } }`.
- **Два таймаута в сумме дают `timeout + 30` секунд.** Агент убивает процесс ровно по `timeout`. Gateway ждёт ещё 30 секунд на финальные события и только потом отдаёт `EXEC_TIMEOUT`.
- **Поддержание соединения в JSON-режиме.** Если команда выполняется дольше 15 секунд, сервер периодически шлёт пробелы в тело ответа — это предотвращает таймауты nginx и промежуточных прокси. Пробелы идут **внутри уже открытого JSON-объекта**, поэтому тело с первого байта выглядит как JSON (`{`), а не начинается с пробелов: и строгие клиенты, и обычные парсеры читают его одинаково, снимать пробелы вручную не нужно.
- **Клиентский таймаут: убедитесь, что он межбайтовый, а не общий.** Keepalive-пробелы сбрасывают таймауты «на тишину», поэтому классические сокет-таймауты Python (`urlopen(..., timeout=300)`, read-таймаут `requests`) сами по себе долгую команду в JSON-режиме не оборвут. Обрывают две другие вещи. Первая — клиенты и обёртки с настоящим wall-clock-дедлайном на весь запрос (например, `aiohttp` с `ClientTimeout(total=…)` или собственный дедлайн агент-фреймворка): keepalive против них бессилен. Вторая — серверный параметр `timeout` самой команды (по умолчанию 300 секунд): по его истечении агент завершает процесс и приходит `EXEC_TIMEOUT` независимо от клиентских настроек — не путайте с обрывом на своей стороне. Практика: в Python задавайте раздельные таймауты — `requests.post(url, json=body, headers=headers, timeout=(10, 60))` — 10 секунд на connect и 60 на паузу между байтами. Если клиент всё же оборвал соединение, лок на сервере держится ещё до `timeout + 90` секунд (`EXEC_BUSY` для повторных вызовов). Команды дольше нескольких минут надёжнее запускать фоновой задачей — см. «Фоновые задачи» ниже. **На спящем сервере это правило вступает в силу не сразу:** пробелы идут только после пробуждения, поэтому до первого байта ответа проходит до 6.5 минут полной тишины, и рецепт `timeout=(10, 60)` оборвёт запрос ещё до запуска команды. Либо поднимите межбайтовый таймаут выше этого окна, либо разбудите машину заранее вызовом [`/wake`](/docs/infra/lifecycle/wake) с `wait=true` и вызывайте `/exec` уже на готовой.

## Принудительное освобождение канала

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

## Как выбрать режим

Три режима вызова и когда каждый уместен:

| Когда | Как | Мониторинг |
|-------|-----|------------|
| Команда укладывается в 600 секунд, прогресс не нужен | `{ "command": "...", "timeout": 600 }` без `?stream=true` — блокирующий JSON-ответ | `exitCode`, `stdout`, `stderr` в теле ответа |
| Команда укладывается в `timeout`, нужен живой прогресс | тот же запрос с `?stream=true` — SSE-события `stdout` / `stderr` / `exit` | читать события по мере поступления |
| Команда дольше 600 секунд или канал не должен быть занят на всё её время | фоновая задача `systemd-run --unit=<name>` — `/exec` возвращается сразу | [`GET /v1/infra/servers/:id/logs?service=<name>`](./logs.md) |

Общий дедлайн запроса на стороне клиента не ставьте меньше значения `timeout` команды — иначе клиент оборвёт соединение раньше, чем сервер успеет ответить. Спящий сервер добавляет к этому дедлайну до 6.5 минут на пробуждение — пример ниже рассчитан на уже запущенную машину. Пример раздельных таймаутов для Python:

```python
import requests
r = requests.post(
    f"{VIBE_URL}/v1/infra/servers/{SERVER_ID}/exec",
    headers={"X-Api-Key": VIBE_API_KEY},
    json={"command": "npm ci --production", "workdir": "/opt/app", "timeout": 600},
    timeout=(10, 60),   # 10 секунд на соединение, 60 на паузу между байтами
)
```

Поднять `timeout` у `/exec` шагу `install` не поможет — у шагов деплоя свой предел в 300 секунд, отдельный от 600 у `/exec`.

**Важно:** умолчание `timeout` — **300 секунд**, а не 600. Потолок 600 существует, но чтобы им воспользоваться, поле надо передать явно. Команда, рассчитанная на девять минут и запущенная без `timeout`, будет снята на пятой.

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

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

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

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

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

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

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

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

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

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

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

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

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

- [Полный деплой](./deploy.md)
- [Снять зависший лок](./lock.md)
- [Логи сервиса](./logs.md)
- [Загрузить файл](./upload.md)
- [Восстановить туннель](/docs/infra/lifecycle/repair)
- [Быстрый цикл выпуска](./fast-cycle.md)
- [Galaxy-приложение](/docs/infra/galaxy)
