Перейти к содержимому
Документация

Контейнерные приложения

Запускайте любой 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.

quickstartbash
# 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.

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. Остановка освобождает место в квоте живых релизов, а стабильный TCP-адрес приложения остаётся зарезервированным между перезапусками. Архивация (inquir apps delete) останавливает все релизы и отвязывает все hostname; после неё имя снова свободно. Через 24 часа сервис и его постоянные тома удаляются безвозвратно; в архиве можно запустить удаление сразу.

TCP-порты#

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

postgresbash
# A database: no HTTP port. It stays private unless :public is requested;
# the platform assigns a stable endpoint port. Health-check the service itself.
inquir apps create db \
  --tcp postgres:5432:public \
  --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  ->  Public TCP · db-abc123.apps.inquir.org:<assigned-port>
# Raw TCP always includes a port; the application subdomain stays stable.
psql "postgres://postgres:secret@db-abc123.apps.inquir.org:<assigned-port>/postgres"
  • public публикует порт за пределы loopback и даёт релизу эндпоинт вида tcp.inquir.org:25432. Без него порт доступен только в приватной сети — как {app}.apps.internal:5432 для Postgres, из других приложений того же рабочего пространства (см. «Приватная сеть» ниже). Публичный TCP-порт доступен любому, кто может достучаться до хоста — платформа не терминирует TLS и не проверяет учётные данные на чистом TCP, поэтому сервис должен сам аутентифицировать клиентов.
  • Платформа резервирует стабильный внешний порт за приложением, а не за одним релизом, поэтому публичная строка подключения переживает перезапуски и повторные деплои. В интерфейсе назначение доступно только для чтения, а публичный доступ остаётся отдельной опцией.
  • HTTP-порты никогда не публикуются отдельно — они обслуживаются через hostname (TLS, маршрутизация по Host, жизненный цикл релиза), поэтому public и hostPort для HTTP-порта отклоняются. У приложения может вообще не быть HTTP-порта.

Приватная сеть#

У каждого приложения в рабочем пространстве есть приватный адрес {app}.apps.internal, который могут разрешить и достичь только другие приложения того же пространства — не интернет и не другое пространство. HTTP отдаётся по http://{app}.apps.internal; каждый TCP-порт сохраняет свой контейнерный порт, поэтому база данных — это {db}.apps.internal:5432. Именно этот адрес нужно писать в DATABASE_URL (или доверить это inquir apps connect):

privatebash
# A database and the service that uses it — both private by default
inquir apps create db --template postgres
inquir apps create api --port 3000
inquir apps connect db api      # DATABASE_URL=postgres://…@db-abc123.apps.internal:5432/… lands in api's env
inquir apps deploy api

# inquir apps status db
#   private:    db-abc123.apps.internal:5432  (postgres)
# inquir apps status api
#   private:    http://api-x1y2z3.apps.internal
#   (no public domain until you ask for one)
inquir apps update api --ingress public    # creates https://api-x1y2z3.apps.inquir.org
inquir apps update api --ingress private   # removes it again; the private address stays
  • Приватный адрес есть всегда и не меняется: он выводится из постоянного слага, переживает повторные деплои, перезапуски и откаты и не требует ни флага public, ни платформенного домена. Превью тоже его получают, поэтому сервис достаёт до своей базы ещё до продвижения в продакшен.
  • Публичный доступ — по запросу. У нового приложения нет публичного домена; «Создать домен» в Настройки → Сеть (или --ingress public) создаёт {app}.apps.…, а «Удалить домен» — в настройках или на вкладке «Домены» — убирает его, не трогая приватный адрес. Публичный TCP остаётся отдельной опцией на каждый порт.
  • Показывается только после проверки. Панель и inquir apps status печатают приватный адрес, когда платформа проверила маршрут этого приложения от начала до конца (через несколько секунд после каждого деплоя). До этого он отображается как «подготавливается» — по имени он никогда не угадывается.

Тома и базы данных#

Том — именованное хранилище, подключённое к приложению: объявите его флагом --volume name:/absolute/path[:ro] (или в runtime.volumes[]), и платформа создаст один управляемый Docker-том на приложение и смонтирует его туда. Физическое имя выводится из приложения, а не из релиза, поэтому тот же том data возвращается при каждом повторном деплое, перезапуске после сбоя и откате. Архивация останавливает контейнеры и назначает безвозвратное удаление сервиса и томов через 24 часа. Из архива можно запустить удаление сразу.

  • Принимаются только логическое имя и абсолютный путь внутри контейнера — никогда host-путь, имя Docker-тома или driver options, — поэтому одно рабочее пространство не может смонтировать хранилище другого. В runtime допускается не более 16 томов; /, /dev, /proc и /sys отклоняются.
  • Writable-том закрепляет приложение за одной репликой (scaling.maxReplicas больше 1 отклоняется), а автоматическое пробуждение по HTTP для приложения с томами не предлагается вовсе — scale-to-zero требует runtime только с HTTP-портами, поэтому приложение с томом работает в режиме always или manual.

На этом стоят шаблоны баз данных: inquir apps create mydb --template postgres (или --template redis) создаёт приложение с постоянным томом, закреплённым публичным TCP-портом (чтобы строка подключения пережила любой повторный деплой), сгенерированным паролем в зашифрованном env и первым релизом — одна команда от пустоты до работающей базы.

inquir apps connection-url mydb печатает живой URL вместе с учётными данными. Чтобы дать доступ другому приложению, не пропуская пароль через человека, выполните inquir apps connect mydb web: сервер сам собирает URL и записывает его в окружение web как DATABASE_URL (REDIS_URL для redis), где он вступит в силу на следующем релизе web. Дашборд намеренно показывает тома и их резервные копии, но не учётные данные.

Резервные копии#

Резервная копия — снимок содержимого одного тома в artifact store платформы, который можно восстановить на тот же том. Копии существуют только там, где для развёртывания настроено объектное хранилище: GET /v1/apps/limits сообщает об этом полем features.volumeBackups, а вкладка Тома скрывает элементы управления, когда его нет.

  • Автоматически: каждое приложение с томом и работающим production-релизом снимается каждые 6 часов; хранятся 7 последних снимков на том.
  • По запросу: inquir apps backups mydb create снимает каждый том, смонтированный production-релизом, и ждёт, пока строки не придут в финальное состояние (--no-wait возвращает управление сразу); inquir apps backups mydb list показывает их, начиная с новых.
  • Crash-consistent: на время копирования контейнер ставится на паузу, поэтому архив — снимок на момент времени, а не обход каталога данных, который продолжает меняться. Копия читается через работающий контейнер, поэтому остановленное приложение зарезервировать нельзя.
  • Восстановление заменяет том. inquir apps backups mydb restore <id> --yes останавливает приложение, заменяет содержимое тома снимком и запускает приложение снова; всё, что записано после этого снимка, теряется. Восстановление и удаление точки восстановления требуют владельца или администратора, как и любое другое действие, затрагивающее production.

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

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

domainbash
# 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 сливаются, а не перезаписываются / архивировать с безвозвратным удалением сервиса и постоянных томов через 24 часа.
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 снимает сборку из очереди.
POST /v1/apps/:id/backups, GET /v1/apps/:id/backupsСнять снимок каждого тома production-релиза (202 — копирование идёт в фоне) / список точек восстановления, начиная с новых.
POST …/backups/:backupId/restore, DELETE …/backups/:backupIdВосстановить снимок поверх его тома, остановив и снова запустив приложение / удалить точку восстановления. Владелец или администратор.
POST /v1/apps/:id/connection-url, POST /v1/apps/:id/connectЖивой URL подключения к базе из шаблона / запись его в окружение другого приложения. Обе — POST и никогда не кэшируются: это единственное место, где API отдаёт сохранённые учётные данные.
GET /v1/apps/limitsКвоты, значения по умолчанию и диапазон host-портов, действующие в этом рабочем пространстве.
curlbash
# 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": "…" }

Плоскость вычислений: ECS без ECS#

За каждым приложением стоит оркестрация в духе AWS ECS — ёмкость, размещение, здоровье и восстановление берёт на себя платформа. Машины (воркеры) регистрируются в управляющем узле и шлют heartbeat; релизы размещаются на них как огороженные аллокации; шлюз ведёт трафик прямо к контейнерам, где бы они ни работали.

  • Переносимые релизы. Каждая сборка пушится в реестр платформы и прибивается по digest — ровно тот же образ стартует на любом воркере: релиз — это байты, а не машина.
  • Размещение. Планировщик выбирает воркер по ёмкости (bin-pack на одной машине, spread на нескольких); реплики одного приложения балансируются на каждый запрос.
  • Здоровье и реплики. Релиз обслуживает трафик только после прохождения healthcheck на том интерфейсе, куда трафик реально приходит. Приложения always масштабируются до 8 реплик; упавшие контейнеры перезапускаются с экспоненциальным бэкоффом.
  • Восстановление. Потерянный воркер обнаруживается по истечению lease, его аллокации переезжают; рестарт воркера не трогает обслуживающие контейнеры и переусыновляет их — трафик ничего не замечает.

Ничего из этого не требует настройки: inquir apps deploy собирает, публикует, размещает и промоутит. Добавить машину в пул — один запуск установщика на хосте с Docker; см. руководство по развёртыванию воркеров в репозитории.

Ограничения#

  • Одна реплика на релиз. Перезапуск — восстановление после падения или пересоздание, которое promote выполняет при удержанных production-переменных, — означает короткое окно 503 на hostname этого релиза. На production-hostname ничего не переключается, пока новый контейнер не станет здоровым.
  • Постоянные тома локальны для одного runtime-хоста. Приложение с томом закреплено за хостом, который его держит: ни репликации, ни живой миграции, и резервные копии — единственная копия данных, покидающая этот хост. Удаление mount-настройки сохраняет Docker-том до очистки оператором. После архивации сервис и его тома удаляются безвозвратно через 24 часа; в архиве можно запустить удаление сразу.
  • Нет автомасштабирования. Ресурсы фиксированы на релиз (--memory, --cpu); масштабируйтесь, промоутя релиз с бо́льшим потолком.