Запускайте любой 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.
# 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 в остальных случаях.
| Проба | Что доказывает |
|---|---|
http | GET по 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-сервер становятся полноценным сервисом:
# 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):
# 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). Затем подтвердите:
# 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-портов, действующие в этом рабочем пространстве. |
# 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); масштабируйтесь, промоутя релиз с бо́льшим потолком.