Defining a deployment.

Two views of the same thing: the deployment.yaml on the left, the equivalent click path through the Control Plane on the right. Read whichever one matches how you work — every example below produces the exact same state on disk.

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:

wizard
Click through Source → Placement → Runtime → Review.
Best for getting started.
yaml
Write the file, paste it, hit Apply.
Best for git-tracked change history.
api
POST /api/deploys + apply.
Best for agents and CI.
Read the schema in full before going beyond the recipes here: docs/yaml-schema.md. The Wizard covers the most common subset; the YAML editor is the surface for everything.
single host · single docker container

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.

yaml deployment.yaml
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]
control plane Wizard → new deploy
  1. Sidebar → Wizard. Entry point stays on new.
  2. Intent — name the deploy hello-web, name the component web.
  3. Source — pick Pre-built Docker image.
  4. Source details — image nginx, tag 1.27-alpine.
  5. Placement — tick web-01. (Don't see it? Nodes → Enroll new daemon first.)
  6. Runtime — port 80:80, restart unless-stopped, healthcheck HTTP / → 200.
  7. Review — the right pane shows the YAML you'd write by hand. Create deploy.
single host · two components · explicit dependency

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.

yaml deployment.yaml
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
control plane Wizard ×2 + Add component
  1. Create the deploy with db first — Wizard, image postgres:16, port 5432:5432, env POSTGRES_*, healthcheck TCP 5432. Create deploy.
  2. Land on Deploys → demo-stack. Click + Add component on the deploy detail page.
  3. Wizard reopens with entry add-component already filled with the parent deploy.
  4. Intent — component api; tick the Depends on box and select db.
  5. Source — Docker image ghcr.io/acme/api:v1.5.
  6. Runtime — port 8080:8080, env DATABASE_URL=…, healthcheck HTTP /health.
  7. Review shows the merged YAML. Apply patch creates v2.
The Wizard creates one component per pass. Adding more uses the 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.
templated config · vault-backed secrets

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.

yaml deployment.yaml (excerpt)
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
control plane YAML editor + Vault

The Wizard intentionally stays simple, so it doesn't expose templates, build steps, or vault references. Reach them through the YAML editor:

  1. Sidebar → Deploys → click your deploy.
  2. Switch to the YAML tab. The editor shows the current version.
  3. Add the config.templates + config.vars + config.secrets blocks. Templates live next to the YAML in your repo (configs/api.env.j2) and are uploaded with the deploy bundle.
  4. Hit Validate. The CP type-checks the schema, resolves {{ vault://… }} references against the configured backend, and refuses the deploy if any are missing.
  5. Diff shows what each daemon would change (file content hash, restart yes/no).
  6. Apply creates a new version and starts the rollout.
Vault integration is Phase 2. Today 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 · cross-host ordering · parallel rollout

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.

yaml deployment.yaml (deployment block)
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
control plane Wizard → Placement + Runtime
  1. In the Wizard, when you reach Placement, tick all the hosts the component should land on (e.g. api-01 and api-02).
  2. On Runtime, the Strategy dropdown unlocks once you've selected more than one host. Pick parallel, sequential, or canary (Phase 2).
  3. Cross-host ordering (depends_on_hosts) is not in the Wizard yet — write it directly in the YAML editor on the deploy detail page.
  4. What you'll see on Apply: 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.

validate
Type-check the YAML, resolve refs, verify secrets exist.
CP only · no daemon traffic
diff
Ask each target daemon what would change. Returns a structured plan.
CP → daemons (read-only)
apply
Execute the plan. Stream runner output back. Record the result.
CP → daemons (writes)
Where you see this in the UI: on the deploy detail page, the buttons run in this order. The Versions list on the right shows every 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.

control plane Deploys → detail
  1. Open the broken deploy.
  2. In the Versions list, find the last known-good vn.
  3. Click Rollback to vn. Confirm in the modal.
  4. The CP runs diff + apply with vn as target. A new version vn+k is recorded with type: rollback.
api scripted equivalent
# 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.

verb
path
what it does
GET
/api/deploys
List deploys visible to the current user (RBAC-filtered).
POST
/api/deploys
Create a new deploy from a YAML body. Returns deploy_id and the initial v1.
GET
/api/deploys/{id}
Read the deploy + every version + apply history.
POST
/api/deploys/{id}/validate
Validate the latest YAML on this deploy without touching daemons.
POST
/api/deploys/{id}/diff
Ask daemons what would change. Returns a per-host structured plan.
POST
/api/deploys/{id}/apply
Run the plan. Stream events back via WS at /ws/events.
POST
/api/deploys/{id}/rollback/{n}
Re-apply version n as a new rollback version.
DELETE
/api/deploys/{id}
Tombstone the deploy. Does not stop the components on the daemons; do that with an apply on an emptied YAML first if you want a clean teardown.

Prerequisites checklist

  • A running CP (Quickstart).
  • An admin account (created via the first-run setup form).
  • At least one daemon enrolled — Nodes → Enroll new daemon 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.yaml in the same directory, or a configured vault backend.