---
title: "Blueprints"
description: "Fleet-wide compose templates that Sencho keeps in sync across the nodes you choose."
---

A **Blueprint** is a docker-compose.yml plus a node selector. Sencho ensures that every node matching the selector runs that stack at the latest revision and reports back when reality drifts from the plan. You declare a stack once; Sencho handles the distribution.

Blueprints live under **Fleet → Deployments**.

<Note>
Blueprints are a Skipper feature. Configuring blueprints requires an admin role; viewers see the catalog read-only.
</Note>

## What problem this solves

Without Blueprints, running the same stack on multiple nodes means SSHing or clicking through each node's stack manager and keeping them in sync by hand. When something drifts (someone restarts a container, edits a compose, or a node forgets to pull a new image) you find out when it breaks.

With Blueprints you get:

- **One declaration covers many nodes.** Pick nodes by label (`production`) or by ID. The set is recomputed every reconciliation tick — adding a node with the right label deploys the stack automatically.
- **Drift detection always on.** Every tick, Sencho compares each target node's actual state to the desired one. You choose what happens when drift is found.
- **Safety rails for stateful workloads.** Blueprints are classified as stateless or stateful at author time; stateful blueprints get explicit confirmation prompts before first deploy and before eviction.

## Anatomy of a Blueprint

| Field | Purpose |
|---|---|
| **Name** | Used as the stack directory on every targeted node (`<COMPOSE_DIR>/<blueprint-name>/`). Lowercase letters, digits, hyphens, and underscores. |
| **Description** | Short prose for the catalog tile and detail header. |
| **Compose** | Standard `docker-compose.yml`. The same file ships to every targeted node. |
| **Selector** | Either `labels` (any/all expressions) or a list of node IDs. |
| **Drift policy** | Observe, Suggest, or Enforce. See below. |
| **Reconciler enabled** | Toggle the reconciliation loop without deleting the blueprint. |

Sencho writes a `.blueprint.json` marker into each targeted node's stack directory. The marker carries the blueprint ID, revision, and the timestamp of the last apply. The reconciler refuses to touch any directory that does not carry a matching marker — so a Blueprint named `nginx` will never overwrite an existing user-authored `nginx` stack on any node.

## Selectors

A **labels** selector matches any node whose labels satisfy the expression:

```
all = [docker]
any = [production, staging]
```

This resolves to nodes that have *every* label in `all` AND *at least one* label in `any`. Either side may be empty. An entirely empty labels selector matches nothing — choose at least one label.

A **nodes** selector picks specific node IDs by hand. Useful when you want a one-off blueprint that runs only on a known node.

Add labels to nodes from **Settings → Nodes** — each node row has a Labels column with a `+` button.

## Drift policy

Drift detection runs every minute regardless of the policy. Only the response differs:

| Mode | What happens on drift |
|---|---|
| **Observe** | Drift surfaces in the deployment table; no notification, no auto-fix. |
| **Suggest** (default) | Sencho dispatches a `blueprint_drift_detected` notification through your notification routes, if any. |
| **Enforce** | Sencho re-deploys the blueprint silently when drift is detected. A notification fires only when an auto-fix attempt fails. |

Even **Observe** keeps Sencho honest about what it found — the deployment row shows "drifted 3h ago: service caddy exited code 1". Silence would forfeit Sencho's authority over your fleet.

For **stateful** blueprints under Enforce, Sencho declines auto-fixes that would destroy named volumes (for example, when you rename a volume in the compose). The drift downgrades to Suggest semantics for that event with the reason `auto-fix declined: would destroy volume data`.

## Stateless vs Stateful Blueprints

Sencho classifies your compose at author time:

- **Stateless** — no persistent volumes detected, or only `tmpfs`. Sencho can deploy and evict freely.
- **Stateful** — named volumes or bind mounts detected. Each node holds its own data; Sencho does not replicate volumes between nodes.
- **State unknown** — `external: true` volumes detected. Sencho cannot prove portability and treats the blueprint as stateful for safety.

The classification appears as a chip on the catalog tile and as a banner above the YAML editor. Click the banner to see exactly what made Sencho classify the way it did.

### Safety rails on stateful blueprints

| Trigger | What Sencho does |
|---|---|
| Selector matches a node that has never run this blueprint | Deployment enters `pending_state_review`. The reconciler refuses to deploy until you click **Confirm deploy** in the deployment table. |
| A node leaves the selector while a deployment is active | Deployment enters `evict_blocked`. The reconciler refuses to evict until you choose **Snapshot, then evict** or **Evict and destroy data**. |
| You target more than one node | The editor warns: "Each node will hold its own data — Sencho does not replicate volumes between nodes." |

Stateless blueprints flow through these states automatically.

## Working with Blueprints

### Create

1. Go to **Fleet → Deployments**.
2. Click **New Blueprint**.
3. Fill in the name, description, compose YAML, selector, and drift policy.
4. Watch the classification banner update as you type — it tells you whether the blueprint is portable or pinned.
5. Click **Create blueprint**. Sencho immediately runs one reconciliation tick.

### Apply on demand

The reconciler runs every minute. To trigger it now (for example, after editing the selector or compose), click **Apply now** on the detail sheet.

### Edit

Click **Edit** on the detail sheet. Editing the compose bumps the revision; the reconciler will redeploy on every targeted node on the next tick. Stateful blueprints follow the volume-destroying drift rule under Enforce.

### Withdraw a single deployment

In the deployment table, click **Withdraw** on the node's row. For stateless blueprints, Sencho runs `docker compose down` and removes the directory. For stateful blueprints, you choose between **Snapshot, then evict** (records the compose definition to fleet snapshots, then evicts) and **Evict and destroy data** (typed-confirm, destroys named volumes).

### Delete the blueprint

Stateless blueprints withdraw all deployments and then delete. Stateful blueprints with active deployments refuse to delete — withdraw each deployment explicitly first.

## Migrating stateful data between nodes (manual)

Sencho's compose-native lane does not include automatic volume shipping. When you move a stateful Blueprint's data from node A to node B, do it by hand:

1. Stop the Blueprint deployment on node A from the deployment table.
2. Use your existing host tooling (`docker run --rm -v <volume>:/data busybox tar -czf - /data > snapshot.tar.gz`, or app-aware tooling such as `pg_basebackup`/`mysqldump`/`mongodump`) to capture the volume.
3. Transfer the artifact to node B and restore it into the named volume.
4. Update the Blueprint's selector to include node B; click **Apply now**.

A future Volume Migration feature will automate this with app-aware backup tooling.

## Troubleshooting

### "Name conflict" on a deployment row

A directory by the blueprint's name already exists on that node and does not carry our `.blueprint.json` marker. Most likely cause: a manually created stack with the same name. Resolution: rename either the existing stack or the blueprint, then click **Apply now**.

### Stateful blueprint stuck in "Awaiting confirmation"

Click **Confirm deploy** on the row, then choose **Deploy fresh**. Sencho will create empty named volumes and start the stack.

### Drift never gets corrected

Confirm the drift policy is `enforce` and the blueprint is enabled. Open the detail sheet to see the deployment row's status and the most recent drift summary. If the drift was caused by a compose change that would destroy named volumes, Enforce intentionally downgrades — change the compose to one that preserves volumes, or withdraw and re-deploy with explicit operator confirmation.

### Cannot disable a blueprint

Blueprints with active or drifted deployments refuse to disable; you would orphan them silently. Withdraw the deployments first, then disable.

## What's not in scope

By design, Blueprints do not include:

- A distributed storage layer (no CSI, no Longhorn-style replication)
- Automatic volume migration between nodes
- Per-node parameter overrides or templating (one compose, all nodes)
- Staged or canary rollouts
- Versioning history with one-click rollback (re-paste the prior compose to revert)

These omissions keep Blueprints honest: a compose-native fleet primitive that distributes the file you already have to the nodes you choose.
