Стек — это набор сервисов, которые работают вместе (например, API и его база данных), описанный в одном файле inquir-compose.yaml и развёртываемый одной командой. Каждый сервис становится обычным приложением в одном проекте. Платформа создаёт их в нужном порядке, генерирует пароли, связывает сервисы по приватным именам и запускает каждый, только когда работают те, от которых он зависит. Повторный запуск обновляет то, что изменилось, и не трогает остальное.
Первый стек: API и база данных#
В папке лежат файл стека и по папке на каждый сервис, который вы собираете. Папка API — обычное приложение со своим inquir.yaml и Dockerfile. PostgreSQL папка не нужна: он запускается из официального образа.
my-app/ ├── inquir-compose.yaml ← the stack └── api/ ├── inquir.yaml ← an ordinary one-service manifest ├── Dockerfile └── server.js
db описан прямо в файле: всё, начиная с imageRef, — ровно то, что было бы в блоке services.web файла inquir.yaml. api ссылается на свою папку. {{generated.db-password}} один раз создаётся на сервере, а {{services.db.privateHost}} — приватное имя базы:
version: 1 metadata: title: { en: API with PostgreSQL, ru: API с PostgreSQL } generate: db-password: { bytes: 24, encoding: hex } # made once, on the server services: db: title: PostgreSQL # postgres:17-alpine, pinned by digest imageRef: docker.io/library/postgres@sha256:eee6c6e1b0894fa4fe0075eb054af9fd1cf39a35321c2d4110626cfe506411fb env: # plain values POSTGRES_USER: app POSTGRES_DB: app PGDATA: /var/lib/postgresql/data/pgdata variables: # secrets and references POSTGRES_PASSWORD: "{{generated.db-password}}" runtime: ports: [{ name: pg, port: 5432, protocol: tcp }] volumes: [{ name: data, mountPath: /var/lib/postgresql/data, sizeMb: 1024 }] resources: { memoryMb: 256, cpuVCpus: 0.25 } healthcheck: { type: command, command: "pg_isready -U app -d app", intervalSeconds: 10, timeoutSeconds: 3 } deployStrategy: recreate networkPolicy: { ingress: private, egress: none } scaling: { activation: always } api: path: ./api # a folder with its own inquir.yaml title: API dependsOn: [db] # starts once db serves variables: DATABASE_URL: "postgres://app:{{generated.db-password}}@{{services.db.privateHost}}:{{services.db.ports.pg}}/app" entrypoint: api # up prints its address
Собственный inquir.yaml API остаётся таким же, каким был бы без стека: порт, проверка работоспособности, ресурсы и публичный адрес.
version: 1 services: web: networkPolicy: { ingress: public } # a public HTTPS address runtime: port: 3000 healthcheck: { type: http, path: /healthz } resources: { cpuVCpus: 0.25, memoryMb: 256 }
Проверьте файл и разверните стек:
cd my-app inquir compose validate # every error and warning, and the plan; nothing is created inquir compose up # project my-app: my-app-db first, then my-app-api inquir compose status my-app
up создаёт проект с именем стека, my-app, и в нём приложения my-app-db и my-app-api. Он генерирует пароль, запускает базу, ждёт, пока она пройдёт проверку работоспособности, затем собирает API, выпускает релиз и печатает его адрес. Пароль не появляется в терминале: он хранится зашифрованным и попадает в оба контейнера как секретная переменная, доступная только продакшену.
Полный пример: inquir-examples/ai-agent-bot-postgres — ИИ-ассистент в Telegram, который хранит историю чатов в собственной базе PostgreSQL.
Файл#
Файл называется inquir-compose.yaml (подойдёт и inquir-compose.yml) и лежит в корне папки стека. Ключи верхнего уровня:
version: 1 # required metadata: {...} # title, description, icon, category, docsUrl, notes inputs: {...} # values asked at deploy time generate: {...} # credentials made on the server, once per stack project: {...} # placement: new (default) | existing, and a name defaults: {...} # location, networkPolicy, scaling, runtime.resources of inline services services: {...} # 1 to 32 services (required) entrypoint: api # the public service up prints (optional) x-common: &common # x-* keys are ignored: a place for YAML anchors LOG_LEVEL: info
Любой другой ключ верхнего уровня — ошибка. Ключи, начинающиеся с x-, игнорируются, поэтому в них удобно держать YAML-якоря, ссылки на них и ключи слияния.
Сервисы#
Сервис описывается одним из двух способов:
- Папкой,
path: ./api: у папки своиinquir.yamlи Dockerfile, и она развёртывается ровно так же, как это сделал быinquir apps deploy. Сборку, порт, проверку работоспособности, ресурсы, тома и сеть задаёт еёinquir.yaml. Эту же папку по-прежнему можно развернуть отдельно. - Прямо в файле: настройки записаны в файле стека. Это в точности блок
services.webфайлаinquir.yaml, и проверяется он по тем же правилам:imageRefилиbuild,runtime,env,scale,networkPolicy,scaling,location. Сервису, описанному в файле, нуженimageRefилиbuild.
Оба вида принимают ещё четыре ключа стека:
title— название в консоли и вstatus.dependsOn— сервисы, которые должны заработать раньше этого.variables— переменные приложения; кромеbuildArgs, это единственное место, где работают ссылки.buildArgs— аргументы сборки для этого развёртывания; без секретов.
imageRef должен быть закреплён по digest. Пишите его в полной форме, с адресом реестра: docker.io/library/postgres@sha256:… для официального образа Docker Hub, docker.io/<user>/<image>@sha256:… для другого образа Docker Hub, ghcr.io/<org>/<image>@sha256:… для другого реестра. Digest покажет docker buildx imagetools inspect postgres:17-alpine.
Обычные, несекретные значения пишите в env блока, а всё секретное или со ссылкой — в variables. env блока работает на платформе с включённым manifest env; на inquir.org он включён. Где он выключен, compose validate предупредит об этом, и такие значения нужно писать в variables.
Ключи сервисов состоят из строчных латинских букв, цифр и дефисов и начинаются с буквы: они становятся частью имён. Каждый сервис — приложение с именем <стек>-<сервис>, например my-app-db; приложение стека из одного сервиса называется так же, как стек.
Входные значения: что спрашивается при развёртывании#
inputs перечисляет то, что указывает тот, кто разворачивает стек: токены, ключи, варианты. Консоль строит по ним форму, а CLI берёт их из --input, --input-file или спрашивает в терминале.
inputs: TELEGRAM_BOT_TOKEN: label: { en: Telegram bot token, ru: Токен Telegram-бота } hint: Send /newbot to @BotFather. secret: true # a password field: never echoed, stored encrypted required: true MODEL: label: Model default: gpt-5-mini maxLength: 128 MODE: label: Mode options: [polling, webhook] # a choice; or [{ value, label }, ...] default: polling OPENAI_API_KEY: label: OpenAI API key secret: true requiredGroup: model-key # at least one input of the group ANTHROPIC_API_KEY: label: Anthropic API key secret: true requiredGroup: model-key ALLOWED_USER_IDS: label: Allowed Telegram user IDs pattern: csv-integers # username, hostname, email, https-url, integer, csv-integers
label(обязательно) иhint— обычный текст или{ en, ru }.secret: true— поле пароля: значение не показывается и хранится зашифрованным. У секрета не бываетdefault.required: trueилиdefault, если значение не указано.options— фиксированный список значений или пар{ value, label }.requiredGroup— хотя бы одно значение группы должно быть задано, например один из нескольких ключей API.pattern— один изusername,hostname,email,https-url,integer,csv-integers; а такжеminLength/maxLength.
Переменная, значение которой целиком состоит из одного необязательного входного значения без значения по умолчанию, оставленного пустым, не создаётся вовсе. Если вокруг ссылки есть другой текст, переменная задаётся как получилось после подстановки. При следующем up значение, которое вы не передали, остаётся прежним.
Генерируемые значения#
generate создаёт учётные данные на сервере из криптостойкого источника случайности: bytes от 16 до 64, encoding — hex, base64 или base64url. Каждое значение создаётся один раз для стека, хранится зашифрованным и используется всеми следующими up; новое значение по запросу создаёт rotate.
generate: db-password: { bytes: 24, encoding: hex } # 48 hex characters session-secret: { bytes: 32, encoding: base64 } services: web: path: ./web variables: SESSION_SECRET: "{{generated.session-secret}}" BOT_NAME: "{{stack.name}}-bot"
Ссылки между сервисами#
Значения в variables и buildArgs могут содержать ссылки; они подставляются при развёртывании стека:
{{inputs.KEY}}— указанное значение или значение по умолчанию.{{generated.key}}— сгенерированное значение.{{services.db.privateHost}}— приватное имя сервиса в приватной сети вашего рабочего пространства.{{services.web.publicUrl}}— его адресhttps://, для сервиса сnetworkPolicy.ingress: public.{{services.db.port}},{{services.db.ports.pg}}— его порт или порт с именемpg.{{services.db.endpoints.rw.privateHost}},{{services.db.ordinals.0.privateHost}}— точка подключения или одна из реплик stateful-сервиса.{{stack.name}}— имя стека.
Больше в файле ничего не подставляется: $VAR остаётся как написано, а {{ в блоке сервиса попадает в контейнер как есть. Одна переменная может ссылаться не больше чем на один другой сервис. Сгенерированное значение или секретное входное значение можно использовать только в variables, но не в buildArgs: аргументы сборки остаются в образе. Переменная с таким значением становится секретом, доступным только продакшену; остальные — обычные переменные. Пароль, ключ или токен, записанный в файле открытым текстом, — ошибка: используйте входное значение или generate. PORT задавать нельзя.
Порядок запуска и готовность#
dependsOn: [db] запускает сервис только после того, как заработали все перечисленные: их продакшен-релиз активен, а реплики проходят собственную проверку работоспособности. Независимые друг от друга сервисы развёртываются параллельно, а сборки одного стека идут по очереди. Цикл зависимостей — ошибка. Ссылка сама по себе порядок не задаёт, поэтому compose validate предупреждает, если сервис использует другой, но не зависит от него. down останавливает сервисы в обратном порядке.
Тома, сеть, регион и масштабирование#
Это обычные ключи inquir.yaml каждого сервиса, поэтому в стеке они значат то же, что и для одного приложения:
runtime.volumes:name,mountPath,sizeMb,readOnly. Том принадлежит одному сервису; общих томов у сервисов не бывает.networkPolicy:ingress—private(по умолчанию) илиpublic,egress—full(по умолчанию) илиnone.entrypointдолжен указывать на публичный сервис.location:autoили регион, напримерnlдля Telegram-бота.scaling:activation: alwaysдля базы данных или бота,httpсminReplicas: 0для сайта, которому можно засыпать.scaleзадаёт реплики, в том числе stateful.
defaults задаёт location, networkPolicy, scaling и runtime.resources сразу всем сервисам, описанным в файле; собственные ключи сервиса важнее. На сервис-папку defaults не действует: всё решает её inquir.yaml. С project: { placement: existing } команда up потребует --project.
Развёртывание#
Из папки. В папке стека inquir compose up упаковывает каждую папку сервиса так же, как inquir apps deploy, и загружает её; больше ничего с вашего компьютера не уходит. Имя стека по умолчанию — имя папки, задать его можно через --name: строчные латинские буквы, цифры и дефисы, до 32 символов. Новый стек получает собственный проект с тем же именем (другое имя — --project-name); --project разворачивает стек в существующий проект. Значения берутся из --input KEY=VALUE (можно повторять), из --input-file .env, или терминал спросит обязательные (секреты не отображаются). up ждёт окончания и показывает ход развёртывания; --no-wait возвращается сразу после постановки в очередь, --json выводит машиночитаемый результат.
# .env next to inquir-compose.yaml: never commit it TELEGRAM_BOT_TOKEN=123456789:AA... OPENAI_API_KEY=... MODEL=gpt-5-mini
Из GitHub. Укажите репозиторий вместо папки: https://github.com/<owner>/<repo>, а за ним при желании #<ref> (ветка, тег или коммит; иначе ветка по умолчанию) и :<папка> (папка с inquir-compose.yaml; иначе корень). Платформа читает эту папку на одном коммите и записывает коммит в стек. Публичные репозитории работают сразу, приватные — через GitHub App вашего рабочего пространства (Подключить GitHub в консоли). Без приложения число чтений ограничено несколькими в час на рабочее пространство, поэтому для репозиториев, которые вы разворачиваете часто, подключите его. Push пока не переразвёртывает стек: снова выполните up.
Из каталога. --catalog <шаблон> разворачивает шаблон из каталога приложений — последнюю версию или @<версия>: те же шаблоны, что предлагает каталог в консоли.
# From the stack folder (the name defaults to the folder's) inquir compose up --name shop --input-file .env inquir compose up --name shop --project my-project --input MODEL=gpt-5-mini # From GitHub: owner/repo, then an optional #ref and :folder inquir compose validate https://github.com/acme/shop#v1.2.0:deploy inquir compose up https://github.com/Inquir-search/inquir-examples:ai-agent-bot-postgres \ --name ai-bot --input-file .env # From the catalog: a template, optionally @version inquir compose up --catalog nginx --name site
Для ИИ-агентов для программирования inquir mcp предоставляет compose_validate, compose_up, compose_status и compose_down с теми же проверками и сообщениями.
Обновление стека#
Снова выполните up с тем же именем. Стек сохраняет сгенерированные значения и указанные ранее входные значения, поэтому передавайте только то, что меняется. Каждый сервис сравнивается с тем, что сейчас работает: неизменённый не трогается, изменённый пересобирается (если изменились его файлы), перенастраивается и выпускается заново. up меняет только переменные, которые записал сам стек; переменные, добавленные вами в консоли, остаются. Новый сервис в файле создаётся. Сервис, удалённый из файла, продолжает работать, и status показывает его как осиротевший; up --prune отправляет его в архив, когда остальные заработают, а его тома сохраняет. Если его ещё использует приложение вне стека, --prune не сработает.
# The same command again: generated values and earlier inputs are kept inquir compose up --name shop inquir compose up --name shop --input MODEL=gpt-5 # change one input inquir compose up --name shop --prune # also archive services removed from the file
Прежде чем что-либо создать, up проверяет весь стек по вашему тарифу: приложения, тома, хранилище, процессор и память каждой реплики. Если стек не помещается, ничего не создаётся, а в ошибке перечислены все превышения. Следующий up учитывает только то, что добавляется.
Статус, остановка и удаление#
status показывает состояние стека и последнее развёртывание, а для каждого сервиса — его приложение, адрес и отметки вроде orphaned или stopped.
down останавливает стек: каждый сервис масштабируется до нуля, сначала зависимые. Приложения, переменные, сгенерированные значения и тома остаются, и следующий up снова запустит стек.
down --delete --confirm <стек> удаляет стек: приложения уходят в архив и удаляются через 30 дней, их тома хранятся по обычным правилам резервного копирования. С --delete-volumes приложения и их данные удаляются сразу и навсегда: 30 дней на восстановление уже не будет. Удаление не выполняется, пока от какого-либо сервиса стека зависит приложение вне его.
inquir compose status shop inquir compose down shop # stop; everything is kept inquir compose up --name shop # start again inquir compose down shop --delete --confirm shop # archive, purged after 30 days inquir compose down shop --delete --delete-volumes --confirm shop # and delete the data now
Замена сгенерированного значения#
inquir compose rotate <стек> <ключ> [<папка>] создаёт новое значение для одного ключа generate и выполняет up: заново выпускаются только сервисы, которые его используют. Для стека, развёрнутого из папки, укажите эту папку (., если вы в папке стека): стек обновляется из неё. Стек из GitHub или каталога помнит свой источник, и папка ему не нужна.
# A stack deployed from a folder: run it there and pass the folder (.) inquir compose rotate shop session-secret . # A GitHub or catalog stack remembers its source: no folder inquir compose rotate openclaw gateway-token
Прочитать или скопировать шаблон из каталога#
inquir compose pull <шаблон>[@<версия>] [<папка>] записывает шаблон из каталога в папку: его inquir-compose.yaml и файлы сервисов. Прочитайте, измените и разверните свою копию через up. inquir compose pack делает обратное: превращает папку в один JSON-пакет.
inquir compose pull nginx ./site # the latest version, as a folder inquir compose pull <template>@<version> ./x # a given version cd site && inquir compose up --name site # deploy your copy inquir compose pack . -o site.json # the opposite: a folder as one bundle
Ограничения стеков#
- От 1 до 32 сервисов в стеке; тариф обычно разрешает меньше, и
upпроверяет это заранее. - До 64 входных и 32 генерируемых значений; файл — не больше 256 КиБ.
- Имя стека — до 32 символов;
<стек>-<сервис>должно помещаться в имя приложения (64 символа). - Не больше 5 развёртываний стеков в очереди или в работе на рабочее пространство и по одному на стек.
- Из репозиториев поддерживается только GitHub (
github.com). - Каждый сервис подчиняется тем же правилам, что и любое приложение: ограничения сборки, проверки работоспособности, регионы, ресурсы тарифа.
Дальше: Контейнерные приложения — всё, что умеет один сервис, inquir.yaml — ключи сервиса, а в готовых рецептах есть Dockerfile, с которого можно начать.