Skip to content
Docs

Stacks: several services from one file

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/bash
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:

inquir-compose.yamlbash
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.

api/inquir.yamlbash
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:

terminalbash
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:

inquir-compose.yamlbash
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 own inquir.yaml and Dockerfile and deploys exactly as inquir apps deploy would deploy it. Its inquir.yaml decides 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.web block of an inquir.yaml, checked by the same rules: imageRef or build, runtime, env, scale, networkPolicy, scaling, location. A service written in place needs imageRef or build.

Either form also takes four keys of the stack:

  • title: the name shown in the console and in status.
  • dependsOn: the services that must be up before this one starts.
  • variables: the application's variables, the only place besides buildArgs where 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.

inquir-compose.yamlbash
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) and hint: plain text or { en, ru }.
  • secret: true: a password field, never shown back, stored encrypted. A secret has no default.
  • required: true, or a default used 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 of username, hostname, email, https-url, integer, csv-integers; and minLength / 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.

inquir-compose.yamlbash
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}}: its https:// address, for a service with networkPolicy.ingress: public.
  • {{services.db.port}}, {{services.db.ports.pg}}: its port, or the port named pg.
  • {{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: ingress private (the default) or public, egress full (the default) or none. entrypoint must name a public service.
  • location: auto or a location, such as nl for a Telegram bot.
  • scaling: activation: always for a database or a bot, http with minReplicas: 0 for a site that may sleep. scale sets 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.

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

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

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

terminalbash
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.

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

terminalbash
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 up checks 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.