Разделы

Контейнерные приложения: деплой, promote и откат долгоживущих сервисов

Запускайте любой OCI-образ как долгоживущий сервис: соберите его из Dockerfile или подтяните из реестра, проверьте health check, получите preview-URL, а затем переведите в production с мгновенным откатом. HTTP-сервисы получают hostname; базы данных и другие TCP-сервисы — адрес, к которому можно подключиться.

Функция — это код, который платформа выполняет внутри собственного образа рантайма, по одному вызову за раз. Приложение — произвольный контейнер, который платформа держит запущенным: без idle-таймаута, с «сырым» reverse proxy (WebSocket и SSE проходят насквозь) и с поддержкой HTTP и чистого TCP — Postgres, Redis, SMTP, SSH. Приложениями управляют на странице Apps, командой inquir apps в CLI или через REST API; словарь везде один и тот же.

Приложения, релизы, preview

  • Приложение — описание сервиса: имя, порты, health check, ресурсы, переменные окружения, политика исходящего трафика. Его правка меняет следующий релиз и никогда — уже запущенный.
  • Релиз — неизменяемый digest образа плюс замороженная копия runtime-спецификации и env, работающие как один контейнер. Жизненный цикл: В очереди → Запуск → Health check → Здоров → Обслуживает → Резерв отката → Остановлен, либо Ошибка. Релиз, не прошедший health check, никогда не получает трафик.
  • Preview — каждый новый релиз поднимается на собственном preview-URL ({app}-{release}.preview.apps.…), чтобы его можно было проверить до того, как его увидит кто-то ещё. Непромоученные preview удаляются через 24 часа.
  • Promote — направляет production-трафик (production-hostname и все собственные домены) на релиз. Делается вручную и доступно только владельцам и администраторам.
  • Откат — снова направляет production на предыдущий релиз. Предыдущий контейнер продолжает работать в течение drain-окна, поэтому пока действует этот резерв, откат мгновенный.

Разверните первый сервис

С установленным и авторизованным CLI HTTP-сервис — это три команды. inquir apps deploy без флагов упаковывает текущий каталог (или --dir), собирает его из Dockerfile на платформе и выпускает релиз из полученного образа; передайте --image, чтобы вместо сборки выпустить готовый образ. Команда ждёт, пока релиз станет здоровым, и печатает его preview-URL.

quickstart
# 1. Create the application — an HTTP service listening on port 3000
inquir apps create web --port 3000

# 2. Deploy: builds the Dockerfile in the current directory, releases the image
#    and waits until the health check passes. The release gets a preview URL.
inquir apps deploy web

#    …or release a prebuilt image instead of building
inquir apps deploy web --image nginx:alpine

# 3. Promote: production traffic moves to this release (owner/admin login)
inquir apps promote <releaseId>

inquir apps status web            # endpoints, hostnames, recent releases
inquir apps logs web --tail 100   # stream container logs
inquir apps rollback web          # back to the previous production release

В дашборде тот же путь: Новое приложение, затем Новый релиз (образ или сборка из исходников) на вкладке «Релизы», затем Promote в строке релиза. В CLI id и имена взаимозаменяемы, а --json принимается везде.

Health check

Новый релиз проходит стартовую проверку здоровья (по умолчанию до 120 с; контейнер инспектируется каждые 2 с, поэтому падение обнаруживается сразу); сервисы с HTTP-портом затем перепроверяются периодически (intervalSeconds, по умолчанию 30 с). Есть три типа проб; по умолчанию используется http, если у приложения есть HTTP-порт, и tcp в остальных случаях.

ПробаЧто доказывает
httpGET по health-пути (по умолчанию /) отвечает статусом ниже 500 — сервер, отдающий 404 на корень, всё же слушает; нездоровыми считаются только ошибки соединения и 5xx.
tcpПорт принимает соединение и ненадолго удерживает его открытым.
commandКоманда, запущенная внутри контейнера, завершается с кодом 0.

Для баз данных используйте command. Docker принимает соединения на опубликованном порту раньше, чем контейнер реально начинает слушать, поэтому tcp-проба может пройти, пока Postgres ещё инициализируется. --health-command "pg_isready -h 127.0.0.1" (или redis-cli ping) спрашивает сам сервис. В API это runtime.healthcheck: { type, path | command, port?, intervalSeconds, timeoutSeconds }.

Preview-URL и production-URL

У приложения с HTTP-портом есть один production-hostname ({app}.apps.… плюс собственные домены), который всегда следует за промоученным релизом, а у каждого релиза — свой preview-URL, пока его не промоутят или не остановят. TLS для обоих терминируется на границе платформы. У чисто TCP-сервиса URL нет вовсе — у него есть эндпоинты (host:port на каждый публичный TCP-порт), которые показывают вкладка «Релизы» и inquir apps status.

Promote, откат, остановка

Promote — осознанный шаг с подтверждением: UI и CLI заранее сообщают, что произойдёт:

  • Если у приложения есть переменные только для production, контейнер пересоздаётся под production-профилем (с проверкой здоровья) до переключения трафика, поэтому preview никогда не несёт production-учётные данные.
  • Preview-URL релиза отзывается, как только он начинает обслуживать production; production-hostname и все подтверждённые собственные домены начинают маршрутизировать на него.
  • Предыдущий production-релиз продолжает работать в течение drain-окна (по умолчанию 5 мин, drainGraceSeconds на релиз) как резерв для мгновенного отката. Текущие запросы и WebSocket на нём завершаются на своих сокетах.

Откат (inquir apps rollback <app>, POST /v1/apps/:id/rollback) возвращает production на предыдущий релиз: мгновенно (200), пока его контейнер ещё в резерве, иначе он сначала перезапускается из собственного замороженного образа (202). Откат сам по себе является promote, поэтому к релизу, с которого вы ушли, применяется то же drain-окно.

Остановка (inquir apps stop <release>, DELETE /v1/releases/:id) останавливает любой релиз, не обслуживающий production, — как preview, так и вытесненные релизы; для маршрутизируемого релиза ответ 409. Так освобождают закреплённый host-порт или место в квоте живых релизов. Архивация (inquir apps delete) останавливает все релизы и отвязывает все hostname; после неё имя снова свободно.

TCP-порты

Порты помимо основного HTTP-порта объявляются флагом --tcp name:port[:public][:hostPort] (или в runtime.ports[] с protocol: tcp). TCP-порт публикуется на host-порту, и к нему подключаются напрямую — без runtime-адаптера и без предположения об HTTP, — поэтому база данных или SSH-сервер становятся полноценным сервисом:

postgres
# A database: no HTTP port, one public TCP port pinned to host port 25432,
# health-checked by asking the service itself (not by a TCP connect)
inquir apps create db \
  --tcp postgres:5432:public:25432 \
  --health-command "pg_isready -h 127.0.0.1" \
  --set POSTGRES_PASSWORD=secret
inquir apps deploy db --image postgres:16-alpine

# inquir apps status db  ->  TCP · tcp.inquir.org:25432 (postgres)
psql "postgres://postgres:secret@tcp.inquir.org:25432/postgres"
  • public публикует порт за пределы loopback и даёт релизу эндпоинт вида tcp.inquir.org:25432. Без него порт привязан к loopback на хосте (полезно только для отладки). Публичный TCP-порт доступен любому, кто может достучаться до хоста — платформа не терминирует TLS и не проверяет учётные данные на чистом TCP, поэтому сервис должен сам аутентифицировать клиентов.
  • hostPort закрепляет опубликованный порт, чтобы строки подключения переживали перезапуск; незакреплённый порт переназначается при каждом пересоздании контейнера. Закрепление должно попадать в диапазон портов платформы (по умолчанию 20000–39999; форма создания показывает актуальный диапазон) и является резервированием на весь хост: второй живой релиз с тем же портом получит 409, пока держатель не остановлен.
  • HTTP-порты никогда не публикуются отдельно — они обслуживаются через hostname (TLS, маршрутизация по Host, жизненный цикл релиза), поэтому public и hostPort для HTTP-порта отклоняются. У приложения может вообще не быть HTTP-порта.

Собственные домены

Помимо сгенерированного hostname, приложение доступно по любому имени, которое вы к нему привяжете — на вкладке Домены или из CLI. Новый hostname сначала не подтверждён: он не маршрутизируется и сертификат не выпускается, пока вы не докажете владение TXT-записью _inquir-verify.<host> с выданным токеном плюс CNAME на domains.inquir.org (или A/ALIAS на ingress). Затем подтвердите:

domain
# Bind the hostname — it starts unverified and prints the DNS records to publish
inquir apps domain shop shop.example.com
#   TXT   _inquir-verify.shop.example.com  ->  <token>
#   CNAME shop.example.com                 ->  domains.inquir.org

# Once DNS has propagated, prove ownership: the hostname becomes routable and gets TLS
inquir apps domain shop shop.example.com --verify

inquir apps domain shop                            # list bound hostnames
inquir apps domain shop shop.example.com --remove  # unbind

Собственные домены — всегда production-hostname и следуют за промоученным релизом с момента подтверждения; добавить домен можно как до, так и после первого promote. Имена, принадлежащие платформе (суффиксы apps/preview, хосты маркетинга и дашборда, hostname, уже занятые собственным доменом шлюза), отклоняются. Изменения доменов переключают production-трафик, поэтому требуют роли владельца или администратора.

Переменные окружения

Задавайте переменные на приложении (--set KEY=VALUE, раздел Переменные в настройках или envVars в API). Значения шифруются при хранении, маскируются при каждом чтении и замораживаются в каждом релизе при его создании — изменение переменной влияет на следующий релиз, а не на запущенный. Чтобы подхватить новые значения, задеплойте заново («Deploy again»).

Ключи из productionOnlyEnvKeys не передаются релизу, пока он работает под профилем preview, и внедряются только при promote (контейнер сначала пересоздаётся под production-профилем). Используйте это для production-учётных данных, чтобы preview-URL, переданный коллеге или агенту, не мог их нести. PORT подставляется автоматически из основного HTTP-порта.

Исходящий трафик

--egress none|full (networkPolicy.egress) определяет, может ли контейнер вообще открывать исходящие соединения. none помещает его в изолированную сеть без маршрута наружу — правильно для базы данных, к которой только подключаются, и неправильно для всего, что вызывает внешние API. Списки разрешённых хостов доступны для функций, но пока не для приложений; API отвечает 400 на allowlist, вместо того чтобы принять политику, которую релиз не сможет соблюсти.

Квоты и значения по умолчанию

У каждого рабочего пространства есть потолок по числу живых релизов и по ресурсам на релиз; форма создания и GET /v1/apps/limits показывают действующие значения. Значения платформы по умолчанию:

ЛимитПо умолчанию
Живых релизов на рабочее пространство (preview + production + резерв)10
Память на релиз512 MB (потолок 2,048 MB)
CPU на релиз0.5 vCPU (потолок 2 vCPU)
Дедлайн стартовой проверки здоровья120 s
Drain-окно (резерв отката)5 min
Время жизни preview-релиза24 h
Перезапусков до пометки релиза как упавшего (crash loop)5

Роли и API-ключи

Любой участник рабочего пространства — включая роль developer — может создавать приложения, деплоить релизы, смотреть логи, останавливать не-production релизы и менять настройки. Promote, откат, собственные домены, архивация и exec требуют владельца или администратора, вошедшего в систему (сессия в браузере или personal access token, выданный inquir login). Действия, недоступные текущей роли, показываются отключёнными с указанием причины.

API-ключи рабочего пространства не могут делать promote, какой бы широкой ни была их роль: promote и exec требуют пользовательской идентичности. Это сделано намеренно — ключ CI или агента со скоупом deploy может собирать и выкатывать preview, читать их статус и логи, но не может переключать production-трафик или читать секреты, которые держит живой контейнер.

REST API

Всё, что делают CLI и дашборд, идёт через /v1/apps…. Долгие операции отвечают 202 и дают URL статуса: строка релиза несёт «голую» ссылку на образ, пока digest не закреплён, затем imageDigest меняется на sha256:….

ЭндпоинтНазначение
POST /v1/apps, GET /v1/appsСоздать приложение (значения env маскируются при каждом чтении) / список с курсорной пагинацией.
GET /v1/apps/:id, PATCH /v1/apps/:id, DELETE /v1/apps/:idПрочитать (production-указатель, URL, preview, домены, последние релизы) / обновить — маскированные значения env сливаются, а не перезаписываются / архивировать.
POST /v1/apps/:id/releasesСоздать релиз из {buildId} или {imageRef, runtime?} → 202; pull и проверка здоровья идут в фоне.
GET /v1/releases/:id, GET /v1/releases/:id/logsПроекция статуса релиза / логи контейнера по SSE.
POST /v1/releases/:id/promote, POST /v1/apps/:id/rollbackПеревести production-трафик на релиз / вернуть на предыдущий (200 мгновенно, 202 с перезапуском). Только вход владельца/администратора.
DELETE /v1/releases/:idОстановить любой не-production релиз; маршрутизируемый отвечает 409.
POST /v1/releases/:id/execВыполнить команду внутри живого контейнера (вывод ограничен 1 MiB; по таймауту дерево процессов убивается). Только вход владельца/администратора.
POST /v1/apps/:id/domains, POST …/domains/:hostname/verifyПривязать собственный hostname (не подтверждён, возвращает TXT-запись) / проверка владения через DNS → маршрутизация + TLS.
POST /v1/apps/:id/builds, GET /v1/builds/:id/logsСобрать образ из снимка исходников или base64-zip → 202 / стрим логов сборки; POST /v1/builds/:id/cancel снимает сборку из очереди.
GET /v1/apps/limitsКвоты, значения по умолчанию и диапазон host-портов, действующие в этом рабочем пространстве.
curl
# Create an application (API key or login)
curl -X POST "https://api.inquir.org/v1/apps" \
  -H "Authorization: Bearer $INQUIR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"web","runtime":{"port":3000}}'

# Release a prebuilt image — 202; the pull and the health gate run in the background
curl -X POST "https://api.inquir.org/v1/apps/{appId}/releases" \
  -H "Authorization: Bearer $INQUIR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"imageRef":"nginx:alpine"}'
# -> { "id": "…", "status": "PENDING" }   poll GET /v1/releases/{releaseId}

# Promote — needs a user session or personal access token; API keys are refused
curl -X POST "https://api.inquir.org/v1/releases/{releaseId}/promote" \
  -H "Authorization: Bearer $INQUIR_PAT"
# -> { "productionUrls": ["https://web.apps.inquir.org"], "previousReleaseId": "…" }

Ограничения

  • Одна реплика на релиз. Перезапуск — восстановление после падения или пересоздание, которое promote выполняет при удержанных production-переменных, — означает короткое окно 503 на hostname этого релиза. На production-hostname ничего не переключается, пока новый контейнер не станет здоровым.
  • Нет постоянных томов. Файловая система контейнера живёт столько же, сколько контейнер: релиз базы данных теряет данные при пересоздании контейнера (перезапуск после падения, повторный деплой, перезапуск при откате). Используйте приложения для stateless-сервисов и dev/test-баз, а долговременные данные храните в управляемом хранилище.
  • Нет автомасштабирования. Ресурсы фиксированы на релиз (--memory, --cpu); масштабируйтесь, промоутя релиз с бо́льшим потолком.