Для AI-агентов: markdown этой страницы — /docs-content/infra/deploy/fast-cycle.md индекс документации — /llms.txt
Быстрый цикл выпуска
Скоуп: vibe:infra
Полный редеплой через POST /v1/infra/servers/:id/deploy прогоняет девять шагов подряд: остановка сервиса, очистка, скачивание архива, установка рантайма, установка зависимостей, запись .env, команды preStart, создание systemd-юнита, запуск и проверка работоспособности. Спящий сервер деплой будит сам. Это правильный путь для первого выпуска и для смены рантайма, но на каждой мелкой правке кода прогонять все девять шагов долго. Ниже — четыре приёма, которые сокращают цикл: убрать раздувание тела запроса, не переустанавливать тяжёлые зависимости, обновлять один изменившийся файл вместо всего дерева, держать данные отдельно от кода.
Что понадобится
- API-ключ Вайбкод со скоупом
vibe:infra - BLACKHOLE-сервер с уже выпущенным приложением — приёмы ниже сокращают повторный выпуск, а не первый
- Архив сборки на рабочей машине
- Node.js 18 или новее для примеров на JavaScript
Во всех примерах $VIBE_URL — базовый адрес https://vibecode.bitrix24.tech, $SERVER_ID — идентификатор BLACKHOLE-сервера, $VIBE_API_KEY — ваш API-ключ.
Какой приём когда применять
Приёмы независимы — берите тот, что отвечает вашей задержке, а не все подряд.
| Что мешает | Приём | Что даёт |
|---|---|---|
| Тело запроса раздувается, выгрузка идёт долго | Multipart вместо base64 | Минус ~33 % объёма, merge-режим по умолчанию |
| Каждый деплой переустанавливает тяжёлые инструменты | Тяжёлые зависимости — вне install-скрипта | Шаг install перестаёт упираться в свои 300 секунд |
| Изменился один файл, а прогоняются все девять шагов | Точечные обновления через /upload |
Обновление без деплоя вообще |
Данные лежат в /opt/app и стираются |
Данные отдельно от кода | cleanDeploy перестаёт быть опасным |
Полный деплой остаётся правильным путём для первого выпуска, смены рантайма и правки start-команды.
Примеры ниже показывают отдельные вызовы и опираются на переменные VIBE_URL, VIBE_API_KEY и SERVER_ID, объявленные в первом блоке JavaScript.
Multipart вместо base64
Встроенный source.content — это base64-строка архива, а base64 увеличивает объём примерно на 33 %: архив 30 МБ уходит по проводу как 40 МБ. Multipart-режим передаёт архив файлом-как-есть, без этой надбавки. Второе отличие — cleanDeploy в multipart-режиме по умолчанию "false" (merge-деплой: новые файлы кладутся поверх существующего дерева), тогда как inline-деплой из JSON по умолчанию cleanDeploy: true и стирает /opt/app перед распаковкой. Для итеративной правки merge-режим быстрее — не нужно каждый раз выкладывать полное дерево.
cURL
tar -czf app.tar.gz -C ./my-app .
curl -sS --fail-with-body -X POST "$VIBE_URL/v1/infra/servers/$SERVER_ID/deploy" \
-H "X-Api-Key: $VIBE_API_KEY" \
-F "file=@app.tar.gz" \
-F "start=cd /opt/app && node server.js" \
-F "port=3000"
JavaScript
import { readFile } from 'node:fs/promises'
const VIBE_URL = process.env.VIBE_URL ?? 'https://vibecode.bitrix24.tech'
const VIBE_API_KEY = process.env.VIBE_API_KEY
const SERVER_ID = process.env.SERVER_ID
const form = new FormData()
form.append('file', new Blob([await readFile('app.tar.gz')]), 'app.tar.gz')
form.append('start', 'cd /opt/app && node server.js')
form.append('port', '3000')
const res = await fetch(`${VIBE_URL}/v1/infra/servers/${SERVER_ID}/deploy`, {
method: 'POST',
headers: { 'X-Api-Key': VIBE_API_KEY },
body: form,
})
const body = await res.json()
if (!body.success) {
// Отказ шага деплоя приходит кодом DEPLOY_FAILED, а имя шага — полем step.
throw new Error(`${body.error?.step ?? 'deploy'}: ${body.error?.message}`)
}
Ответ перечисляет пройденные шаги:
{
"success": true,
"data": {
"steps": [
{ "step": "stop_existing", "status": "ok", "duration": 320 },
{ "step": "clean", "status": "ok", "duration": 180 },
{ "step": "install", "status": "ok", "duration": 21350 },
{ "step": "start", "status": "ok", "duration": 900 }
],
"serviceName": "app",
"status": "running",
"appUrl": "https://app-b7c1e2a4f9d0.vibecode.bitrix24.tech"
}
}
У шага четыре возможных состояния: running, ok, warning и error. Шаг со статусом warning деплой не прерывает — пояснение приходит в stdout этого шага.
Порт в multipart-режиме передавать обязательно — строкой ("3000"). Чтобы вместо merge сделать чистый деплой, добавьте -F "cleanDeploy=true" (в JavaScript — form.append('cleanDeploy', 'true')).
Тяжёлые зависимости — вне install-скрипта
У шага install таймаут 300 секунд, и запускается он на каждом деплое. Долгая установка набора инструментов сборки на этом шаге либо упирается в таймаут, либо отнимает минуты у каждого деплоя. Файловая система виртуальной машины помогает этого избежать: cleanDeploy стирает только extractTo (по умолчанию /opt/app), а установленные рантаймы, каталог /usr и /opt/data переживают редеплой. Поэтому системный инструмент ставится один раз и остаётся на месте.
Установите тяжёлый инструмент однажды и в дальнейшем проверяйте его наличие идемпотентной строкой — она запускает установочный скрипт, только если инструмента ещё нет:
command -v pg_restore >/dev/null || bash /opt/data/install-tools.sh
Строку помещают в preStart. При первом деплое pg_restore не найден — скрипт отработает. На следующих деплоях инструмент уже на месте, проверка проходит мгновенно, установка не повторяется. Так каждый деплой не платит за переустановку того, что уже стоит на диске.
Точечные обновления через /upload
Когда изменился один файл или один бандл, полный редеплой избыточен. Загрузите изменившийся архив через POST /v1/infra/servers/:id/upload с extract: true — агент распакует его прямо в рабочую директорию, минуя остальные восемь шагов деплоя. Затем перезапустите сервис через /exec:
curl -sS --fail-with-body -X POST "$VIBE_URL/v1/infra/servers/$SERVER_ID/upload" \
-H "X-Api-Key: $VIBE_API_KEY" -H "Content-Type: application/json" \
-d '{
"url": "https://example.com/dist.tar.gz",
"path": "/opt/app/dist.tar.gz",
"extract": true,
"extractTo": "/opt/app"
}'
curl -sS --fail-with-body -X POST "$VIBE_URL/v1/infra/servers/$SERVER_ID/exec" \
-H "X-Api-Key: $VIBE_API_KEY" -H "Content-Type: application/json" \
-d '{"command": "systemctl restart app"}'
JavaScript
const headers = { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' }
const uploaded = await fetch(`${VIBE_URL}/v1/infra/servers/${SERVER_ID}/upload`, {
method: 'POST',
headers,
body: JSON.stringify({
url: 'https://example.com/dist.tar.gz',
path: '/opt/app/dist.tar.gz',
extract: true,
extractTo: '/opt/app',
}),
}).then(r => r.json())
// Перезапуск вслепую, без этой проверки, поднимет прежнюю сборку и создаст
// впечатление, что правка не применилась.
if (!uploaded.success) throw new Error(uploaded.error?.message ?? 'файл не загружен')
const restarted = await fetch(`${VIBE_URL}/v1/infra/servers/${SERVER_ID}/exec`, {
method: 'POST',
headers,
body: JSON.stringify({ command: 'systemctl restart app' }),
}).then(r => r.json())
if (!restarted.success) throw new Error(restarted.error?.message ?? 'сервис не перезапущен')
if (restarted.data.exitCode !== 0) throw new Error(restarted.data.stderr)
Ответ /upload подтверждает путь, размер и факт распаковки:
{ "success": true, "data": { "path": "/opt/app/dist.tar.gz", "size": 4193280, "extracted": true } }
Если предыдущая команда на сервере ещё выполняется, перезапуск через /exec отклоняется:
{
"success": false,
"error": {
"code": "EXEC_BUSY",
"message": "Another operation is running on this server",
"retryable": true,
"retryAfter": 10,
"hint": {
"reason": "A 'deploy' operation currently holds the lock on this server.",
"recovery": "If the previous operation crashed or its deploy task is stuck (e.g. the backend restarted, or the server was deleted and recreated), force-release the lock and retry.",
"recoveryAction": "DELETE /v1/infra/servers/:id/lock",
"autoExpiresInSeconds": 42,
"note": "The backend lock auto-expires after ~15 minutes. The Black Hole agent also holds its own exec mutex (\u226410 min) that releases when the running command finishes or times out. If force-releasing the backend lock STILL yields EXEC_BUSY, the agent exec mutex has leaked (a detached background process is holding it open) — call POST /v1/infra/servers/:id/unstick to force-release the lock AND bounce the agent tunnel (its reconnect handler group-kills the stuck exec, freeing the mutex) with no VM reboot."
}
}
}
Поля retryable и retryAfter — машинный сигнал: повтор уместен, пауза в секундах (не больше 10 — это интервал опроса, а не срок жизни лока), она же приходит заголовком Retry-After. Лок снимается сам примерно через 15 минут, а зависший снимается вручную через DELETE /v1/infra/servers/:id/lock.
Перезапуск запускайте только после успешной загрузки: если /upload вернул отказ, а systemctl restart уже ушёл, сервис поднимется на старом коде и это будет выглядеть как «деплой не применился».
Имя сервиса в команде перезапуска — то, под которым приложение задеплоено. По умолчанию это app, поэтому и systemctl restart app. Загрузка размером до 500 МБ — с ней проходит и целый бандл, и отдельный файл.
Данные — никогда в install-шаге
Шаг install (и preStart) выполняется на каждом деплое. Если положить в него загрузку данных — вставку строк в базу, импорт справочника, накат начальных данных — эта загрузка повторится при каждом деплое, и записи задвоятся. Данные грузят в /opt/data один раз и под идемпотентной защитой, чтобы повторный запуск ничего не добавлял:
test -f /opt/data/.seeded || { bash /opt/data/load-seed.sh && touch /opt/data/.seeded; }
Каталог /opt/data переживает редеплой, поэтому файл-маркер .seeded остаётся на месте между деплоями. Первый деплой загрузит данные и поставит маркер, все следующие увидят маркер и пропустят загрузку. Команды-шаги деплоя держите для установки кода и зависимостей, а разовую загрузку данных выносите под такую защиту.
Известные особенности
Умолчание зависит от режима. Умолчание cleanDeploy зависит от режима: у inline-деплоя из JSON это true и /opt/app стирается перед распаковкой, у multipart — false, то есть файлы кладутся поверх дерева. Режимы не взаимозаменяемы по последствиям.
Потолок шага install. Шаг install запускается на каждом деплое и ограничен 300 секундами. Тяжёлая установка либо упирается в таймаут, либо отнимает минуты у каждого выката.
/opt/app не переживает чистый деплой. Каталог /opt/app живёт до следующего чистого деплоя. Данные, которые должны пережить выкат, кладутся в /opt/data, а не в дерево приложения.
В /opt/data пишет root, а не приложение — пока каталог не объявлен. Все примеры выше выполняются на шагах install и preStart, то есть от root, и это работает как написано. Само приложение systemd запускает под непривилегированной учётной записью, которой платформа отдаёт только extractTo, — поэтому его собственная запись в /opt/data завершится ошибкой доступа. Чтобы приложение писало туда само, объявите каталог в теле деплоя:
{ "dataDirs": ["/opt/data/state"] }
Платформа создаст каталог и передаст его учётной записи приложения на каждом деплое.
Объявляйте отдельный подкаталог, а не сам /opt/data. Владелец каталога может удалить или заменить в нём любой файл — даже те, что положил root и читать которые не может. В /opt/data по примерам выше лежат install-tools.sh и load-seed.sh, которые запускаются от root на каждом деплое: отдав приложению весь /opt/data, вы дали бы ему возможность подменить эти скрипты. Приложению — свой подкаталог (/opt/data/state), служебным скриптам и файлам с паролями — сам /opt/data, который вы не объявляете. Передаётся при этом сам каталог, а не его содержимое: файлы, положенные root раньше, владельца не меняют. Подробности — «Деплой приложения».
Потолок загрузки — 500 МБ. Маршрут /upload принимает до 500 МБ на файл, всё, что больше, отклоняет.
Отказы зависят от того, чей это вызов. У прямого вызова /exec: EXEC_TIMEOUT — команда не уложилась в свой timeout, COMMAND_TOO_LONG — команда длиннее 10000 символов, EXEC_BUSY — на сервере уже идёт операция.
У деплоя ответ другой. Шаг, не уложившийся в свои 300 секунд, приходит как DEPLOY_FAILED с полем step — например "step": "install", — а текст таймаута лежит в message. Ветвиться на EXEC_TIMEOUT после деплоя бесполезно, этот код там не приходит.
Полный перечень кодов — Ошибки.