What a deploy actually is
A deploy in Maestro is a named, versioned bundle
of YAML. Every time you submit a new YAML the CP creates a new
deploy_version (immutable, numbered v1,
v2, …). Apply happens on a single version: the CP
sends the spec to each daemon listed under
deployment, the daemon reconciles its host, and the
CP records the result. Rollback flips the active version pointer
back to a previous one and re-applies it.
Three ways to produce a YAML, three places where the same field shows up:
Apply.POST /api/deploys + apply.Hello, Nginx
The smallest meaningful deploy: one host, one prebuilt Docker image, one published port. This is exactly what the Wizard produces if you accept all defaults.
api_version: maestro/v1 project: hello-web hosts: web-01: type: linux address: 10.0.0.10 components: web: source: type: docker image: nginx tag: "1.27-alpine" run: type: docker container_name: hello-web ports: ["80:80"] restart: unless-stopped healthcheck: type: http url: http://localhost/ expect_status: 200 deployment: - host: web-01 components: [web]
- Sidebar → . Entry point stays on
new. - Intent — name the deploy
hello-web, name the componentweb. - Source — pick
Pre-built Docker image. - Source details — image
nginx, tag1.27-alpine. - Placement — tick
web-01. (Don't see it? first.) - Runtime — port
80:80, restartunless-stopped, healthcheck HTTP/→ 200. - Review — the right pane shows the YAML you'd write by hand. .
API + Postgres (depends_on)
A real stack is at least two services that need to come up in the
right order. Use depends_on on the dependent
component — the CP builds the graph and rolls out in topological
order, waiting for each healthcheck.
api_version: maestro/v1 project: demo-stack hosts: app-01: { type: linux, address: 10.0.0.20 } components: db: source: type: docker image: postgres tag: "16" run: type: docker container_name: demo-db ports: ["5432:5432"] env: POSTGRES_DB: demoapp POSTGRES_USER: demoapp POSTGRES_PASSWORD: "changeme" healthcheck: type: tcp port: 5432 start_period: 20s api: source: type: docker image: ghcr.io/acme/api tag: "v1.5" run: type: docker container_name: demo-api ports: ["8080:8080"] env: DATABASE_URL: "postgres://demoapp:changeme@localhost:5432/demoapp" healthcheck: type: http url: http://localhost:8080/health expect_status: 200 depends_on: [db] deployment: - host: app-01 components: [db, api] strategy: sequential
- Create the deploy with
dbfirst — Wizard, imagepostgres:16, port5432:5432, envPOSTGRES_*, healthcheck TCP5432. . - Land on . Click on the deploy detail page.
- Wizard reopens with entry
add-componentalready filled with the parent deploy. - Intent — component
api; tick the Depends on box and selectdb. - Source — Docker image
ghcr.io/acme/api:v1.5. - Runtime — port
8080:8080, envDATABASE_URL=…, healthcheck HTTP/health. - shows the merged YAML. creates v2.
add-component entry point, which patches
the existing YAML and bumps the version. If you'd rather
edit the YAML directly, use the Edit YAML
tab on the deploy detail page.
Templates and secrets
For anything beyond plain env-vars, use
config.templates. The CP renders a Jinja2 template
into a target path on the host, with config.vars as
plaintext interpolation and config.secrets resolved
from the vault backend at apply time. Secrets never appear in
the on-disk YAML.
components: api: source: type: git repo: https://github.com/acme/api.git ref: main build: - command: npm ci - command: npm run build config: templates: - source: configs/api.env.j2 dest: /etc/acme/api.env mode: "0640" vars: DB_HOST: "{{ hosts['db-01'].address }}" DB_PORT: 5432 LOG_LEVEL: info secrets: DB_PASSWORD: "{{ vault://db/password }}" JWT_SECRET: "{{ vault://jwt/secret }}" run: type: systemd unit_name: acme-api command: /usr/bin/node /opt/acme-api/dist/server.js working_directory: /opt/acme-api user: deploy restart: on-failure healthcheck: type: http url: http://localhost:3000/health expect_status: 200
The Wizard intentionally stays simple, so it doesn't expose templates, build steps, or vault references. Reach them through the YAML editor:
- Sidebar → → click your deploy.
- Switch to the tab. The editor shows the current version.
- Add the
config.templates+config.vars+config.secretsblocks. Templates live next to the YAML in your repo (configs/api.env.j2) and are uploaded with the deploy bundle. - Hit . The CP type-checks the schema, resolves
{{ vault://… }}references against the configured backend, and refuses the deploy if any are missing. - shows what each daemon would change (file content hash, restart yes/no).
- creates a new version and starts the rollout.
vault://… references work against the local
encrypted credentials file (default
credentials.yaml next to the deployment).
HashiCorp Vault and cloud KMS backends are scheduled for
Phase 2 — see credentials_ref in the schema.
Multi-host with rollout strategy
When a deploy spans more than one host, two knobs matter:
strategy (per host group, controls intra-group
rollout) and depends_on_hosts (cross-group ordering
— wait for these hosts to go green before starting). Strategy
parallel hits everyone at once;
sequential walks them one by one with a healthcheck
in between.
hosts: db-01: { type: linux, address: 10.0.0.20 } api-01: { type: linux, address: 10.0.0.21 } api-02: { type: linux, address: 10.0.0.22 } web-01: { type: linux, address: 10.0.0.23 } # components: { db, api, web } as before… deployment: - host: db-01 components: [db] - host: api-01 components: [api] depends_on_hosts: [db-01] strategy: sequential - host: api-02 components: [api] depends_on_hosts: [db-01] strategy: sequential - host: web-01 components: [web] depends_on_hosts: [api-01, api-02] strategy: parallel
- In the Wizard, when you reach Placement, tick all the hosts the component should land on (e.g.
api-01andapi-02). - On Runtime, the Strategy dropdown unlocks once you've selected more than one host. Pick
parallel,sequential, orcanary(Phase 2). - Cross-host ordering (
depends_on_hosts) is not in the Wizard yet — write it directly in the YAML editor on the deploy detail page. - What you'll see on : each binding becomes a row in the rollout panel. Sequential ones light up one at a time; parallel ones light up together. Failure on any host pauses the rollout for the bindings that depend on it.
Validate → Diff → Apply
Every change goes through the same three-step pipeline. Each
step is a separate API call, so an agent can pause between any
two — most conversations end after diff, with the
human saying "go" before apply runs.
v1, v2, v3… that's
been applied; click one to view its YAML and the apply log.
Rollback
Rollback uses the same pipeline with a previous version as target. The CP doesn't delete the failing version — it just flips the active pointer back and re-applies. The bad version stays in the history for forensics.
- Open the broken deploy.
- In the Versions list, find the last known-good
vn. - Click . Confirm in the modal.
- The CP runs
diff+applywith vn as target. A new versionvn+kis recorded with type: rollback.
# list versions: $ curl -s https://cp/api/deploys/dpl_abc123 | jq .versions # roll back to v3: $ curl -X POST https://cp/api/deploys/dpl_abc123/rollback/3 \ -H "cookie: session=…"
Deploy endpoints
All endpoints live under /api. Auth is a session
cookie obtained from POST /api/auth/login. For
agents, the same surface is exposed as MCP verbs — see
SKILL.md.
deploy_id and the initial v1./ws/events.Prerequisites checklist
- A running CP (Quickstart).
- An admin account (created via the first-run setup form).
- At least one daemon enrolled — in the SPA, or the CLI installer with a token.
- A reachable Docker registry (or a Git URL) for the component
source. - For templated configs: the templates committed alongside the YAML in your repo.
- For secrets:
credentials.yamlin the same directory, or a configured vault backend.