A stack is a set of services that work together, an API and its database for example, described in one file, inquir-compose.yaml, and deployed with one command. Each service becomes an ordinary application in one project. The platform creates them in the right order, generates their passwords, connects them by private names and starts each one only when the services it needs are up. A later run updates what changed and leaves the rest alone.
A first stack: an API and its database#
The folder holds the stack file and one folder per service you build. The API folder is an ordinary application with its own inquir.yaml and Dockerfile; PostgreSQL needs no folder, it runs the official image.
my-app/ ├── inquir-compose.yaml ← the stack └── api/ ├── inquir.yaml ← an ordinary one-service manifest ├── Dockerfile └── server.js
db is written in place: everything from imageRef down is exactly what the services.web block of an inquir.yaml would say. api points to its folder. {{generated.db-password}} is made on the server once, and {{services.db.privateHost}} is the database's private name:
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
The API's own inquir.yaml stays what it would be without the stack: its port, health check, resources and a public address.
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 }
Check the file, then deploy it:
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 creates a project named after the stack, my-app, with the applications my-app-db and my-app-api. It generates the password, starts the database, waits until it passes its health check, then builds the API, releases it and prints its address. The password never shows up in your terminal: it is stored encrypted and reaches both containers as a production-only secret variable.
A complete example: inquir-examples/ai-agent-bot-postgres, an AI assistant for Telegram that keeps chat history in its own PostgreSQL.
The file#
The file is called inquir-compose.yaml (inquir-compose.yml works too) and sits at the top of the stack folder. Its top-level keys:
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
Any other top-level key is an error. Keys that start with x- are ignored, so they are a place for YAML anchors, aliases and merge keys.
Services#
A service is written in one of two ways:
- A folder,
path: ./api: the folder has its owninquir.yamland Dockerfile and deploys exactly asinquir apps deploywould deploy it. Itsinquir.yamldecides the build, port, health check, resources, volumes and network. The same folder still deploys alone. - In place: the settings written in the stack file. They are exactly the
services.webblock of aninquir.yaml, checked by the same rules:imageReforbuild,runtime,env,scale,networkPolicy,scaling,location. A service written in place needsimageReforbuild.
Either form also takes four keys of the stack:
title: the name shown in the console and instatus.dependsOn: the services that must be up before this one starts.variables: the application's variables, the only place besidesbuildArgswhere references work.buildArgs: build arguments for this deploy; no secrets.
imageRef must be pinned by digest. Write it in full registry form, with the registry host: docker.io/library/postgres@sha256:… for an official Docker Hub image, docker.io/<user>/<image>@sha256:… for another Docker Hub image, ghcr.io/<org>/<image>@sha256:… for another registry. docker buildx imagetools inspect postgres:17-alpine prints the digest.
Plain values that are not secret go into the block's env; anything secret or with a reference goes into variables. A block's env needs a platform with manifest env enabled; inquir.org has it. Where it is not, compose validate warns, and those values belong in variables.
Service keys are lowercase letters, digits and dashes, starting with a letter: they become part of names. Each service is an application named <stack>-<service>, such as my-app-db; the application of a one-service stack is named like the stack.
Inputs: values asked at deploy time#
inputs lists what the person who deploys the stack provides: tokens, keys, options. The console builds its form from them; the CLI takes them from --input, --input-file or a prompt.
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(required) andhint: plain text or{ en, ru }.secret: true: a password field, never shown back, stored encrypted. A secret has nodefault.required: true, or adefaultused when nothing is given.options: a fixed list of values, or of{ value, label }pairs.requiredGroup: at least one input of the group needs a value, for example one of several API keys.pattern: one ofusername,hostname,email,https-url,integer,csv-integers; andminLength/maxLength.
A variable whose whole value is one optional input left blank, with nothing else around it and no default, is not set at all. A value with other text around the reference is set as rendered. On a later up, an input you do not pass keeps the value it had.
Generated values#
generate makes credentials on the server from a secure random source: bytes from 16 to 64, encoding hex, base64 or base64url. Each value is made once per stack, stored encrypted and reused by every later up; rotate makes a new one on purpose.
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"
References between services#
Values in variables and buildArgs may contain references, resolved when the stack deploys:
{{inputs.KEY}}: the value given, or the input's default.{{generated.key}}: a generated value.{{services.db.privateHost}}: the service's private name on your workspace network.{{services.web.publicUrl}}: itshttps://address, for a service withnetworkPolicy.ingress: public.{{services.db.port}},{{services.db.ports.pg}}: its port, or the port namedpg.{{services.db.endpoints.rw.privateHost}},{{services.db.ordinals.0.privateHost}}: an endpoint or one replica of a stateful service.{{stack.name}}: the stack's name.
Nothing else in the file is filled in: $VAR stays as written, and {{ in a service block reaches the container as it is. A variable may reference at most one other service. A generated value or a secret input is allowed only in variables, never in buildArgs, because build arguments stay in the image. A variable that holds one becomes a production-only secret; other values are ordinary variables. A literal password, key or token anywhere in the file is an error: use an input or generate. PORT cannot be set.
Start order and readiness#
dependsOn: [db] starts a service only once every listed service is up: its production release is active and its replicas pass their own health check. Services that do not depend on each other deploy side by side, while the builds of one stack take turns. A cycle is an error. A reference alone does not order anything, so compose validate warns when a service uses another without depending on it. down stops services in the reverse order.
Volumes, network, location and scaling#
These are ordinary inquir.yaml keys of each service, so they mean in a stack what they mean for one application:
runtime.volumes:name,mountPath,sizeMb,readOnly. A volume belongs to one service; services cannot share one.networkPolicy:ingressprivate(the default) orpublic,egressfull(the default) ornone.entrypointmust name a public service.location:autoor a location, such asnlfor a Telegram bot.scaling:activation: alwaysfor a database or a bot,httpwithminReplicas: 0for a site that may sleep.scalesets replicas, stateful replicas included.
defaults sets location, networkPolicy, scaling and runtime.resources for every service written in place; a service's own keys win. A folder service is not affected: its inquir.yaml decides. project: { placement: existing } makes up ask for --project.
Deploy#
From a folder. In the stack folder, inquir compose up packs each service folder the way inquir apps deploy does and uploads it; nothing else leaves your computer. The stack name defaults to the folder name, and --name sets it: lowercase letters, digits and dashes, up to 32 characters. A new stack gets a project of its own named after it (--project-name to choose another name); --project deploys into an existing project. Values come from --input KEY=VALUE (repeatable), from --input-file .env, or the terminal asks for the required ones (secrets are not echoed). up waits and shows progress; --no-wait returns once the deployment is queued, --json prints machine-readable output.
# .env next to inquir-compose.yaml: never commit it TELEGRAM_BOT_TOKEN=123456789:AA... OPENAI_API_KEY=... MODEL=gpt-5-mini
From GitHub. Pass a repository instead of a folder: https://github.com/<owner>/<repo>, then optionally #<ref> (a branch, tag or commit; the default branch otherwise) and :<folder> (the folder with inquir-compose.yaml; the root otherwise). The platform reads that folder at one commit and records the commit on the stack. Public repositories work as they are, private ones through your workspace's GitHub App (Connect GitHub in the console). Reads without the app are limited to a few an hour per workspace, so connect it for repositories you deploy often. A push does not redeploy a stack yet: run up again.
From the catalog. --catalog <template> deploys a template of the Applications catalog, the latest version or @<version>: the same templates the console's catalog offers.
# 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
For coding agents, inquir mcp offers compose_validate, compose_up, compose_status and compose_down, with the same checks and messages.
Update a stack#
Run up again with the same name. The stack keeps its generated values and the inputs given before, so pass only what changes. Every service is compared with what runs: an unchanged service is left alone, a changed one is built again if its files changed, reconfigured and released. up changes only the variables the stack wrote; variables you added in the console stay. A new service in the file is created. A service removed from the file keeps running and status reports it as orphaned; up --prune archives it once the rest is up, keeping its volumes. An application outside the stack that still uses it stops the 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
Before anything is created, up checks the whole stack against your plan: applications, volumes, storage, and the CPU and memory of every replica. If it does not fit, nothing is created and the error lists every overrun. A later up counts only what it adds.
Status, stop and delete#
status shows the stack's state and its last deployment, and for each service its application, its address and marks such as orphaned or stopped.
down stops a stack: every service scales to zero, the ones that depend on others first. Applications, variables, generated values and volumes stay, and the next up starts it again.
down --delete --confirm <stack> deletes it: the applications are archived and purged after 30 days, their volumes kept with the usual backup retention. --delete-volumes instead deletes the applications and their data permanently, at once: there are no 30 days to restore them. Delete is refused while an application outside the stack depends on one of its services.
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
Rotate a generated value#
inquir compose rotate <stack> <key> [<folder>] makes a new value for one generate key and runs up: only the services that use it are released again. For a stack deployed from a folder, pass that folder (. in the stack folder), because the stack is updated from it. A GitHub or catalog stack remembers its source and needs no folder.
# 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
Read or copy a catalog template#
inquir compose pull <template>[@<version>] [<folder>] writes a catalog template into a folder: its inquir-compose.yaml and service files. Read it, change it and deploy your copy with up. inquir compose pack does the opposite: it turns a folder into one JSON bundle.
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
Stack limits#
- 1 to 32 services in a stack; your plan usually allows fewer, and
upchecks that first. - Up to 64 inputs and 32 generated values; the file is at most 256 KiB.
- A stack name of up to 32 characters;
<stack>-<service>must fit an application name (64 characters). - At most 5 stack deployments queued or running per workspace, one at a time per stack.
- GitHub only (
github.com) for repositories. - Each service follows the rules of any application: build limits, health checks, regions, plan resources.
Next: Container applications covers everything one service can do, inquir.yaml lists the keys of a service, and Deploy recipes give you a Dockerfile to start from.