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

<Frame>
  <img src="/images/blueprint-model/catalog.png" alt="Blueprint catalog in Fleet Deployments showing blueprint cards, deployment counts, drift policy, and the New Blueprint action" />
</Frame>

## 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, and 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. The YAML must parse successfully and stay under 96 KiB. |
| **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.

Before a local Blueprint deploy starts, Sencho applies the same pre-deploy vulnerability policy gate used by standard stack deploys. If an enabled policy blocks one of the Blueprint's image references, the deployment row moves to failed and the stack is not written to disk. Successful local Blueprint deploys also trigger the normal post-deploy scan. Remote Blueprint deploys are routed through the remote node's stack deploy endpoint, so policy enforcement runs on the remote instance with that node's credentials and scanner state.

<Frame>
  <img src="/images/blueprint-model/editor-dialog.png" alt="New Blueprint editor dialog with name, compose YAML, selector, drift policy, and classification banner" />
</Frame>

## 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, so 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 stateful or unknown blueprint revision changes on a node that already runs it | Deployment enters `pending_state_review`. Confirm the redeploy before Sencho writes the new compose revision. |
| 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.

<Frame>
  <img src="/images/blueprint-model/detail-state-review.png" alt="Blueprint detail sheet with a deployment row waiting for state review confirmation" />
</Frame>

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

If the YAML is malformed or larger than 96 KiB, Sencho rejects the save before creating a Blueprint row.

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

For stateful or state-unknown Blueprints, a compose edit does not redeploy automatically. Each existing deployment enters **Awaiting confirmation** so you can decide whether the new revision is safe for that node's local data.

<Frame>
  <img src="/images/blueprint-model/detail-sheet.png" alt="Blueprint detail sheet showing deployment rows, Apply now, Edit, Delete, and the current compose content" />
</Frame>

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

<Note>
**Snapshot, then evict** captures the compose definition only. Volume bytes are not shipped. The named volumes managed by this stack on the target node are removed by `docker compose down` just as with **Evict and destroy data**. To preserve data, capture volumes manually before withdrawing (see *Migrating stateful data between nodes* below).
</Note>

### 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. **Snapshot, then evict** is a compose-only safety net: it preserves the YAML so you can redeploy elsewhere, but it does not move data. To relocate a stateful Blueprint's data from node A to node B, do it by hand before withdrawing:

1. Stop the Blueprint deployment on node A from the deployment table. *Tip:* use **Snapshot, then evict** so the compose YAML is parked in Fleet → Snapshots while you handle volumes.
2. Use your 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 on node A.
3. Transfer the artifact to node B and restore it into the named volume there.
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. If the row appeared after a compose edit, review the revision first; confirming writes the new compose file and deploys it on that node.

### Deploy blocked by vulnerability policy

Open **Settings → Security** and review the active scan policies for the target node. The Blueprint deployment row records the blocking policy and affected image count. Either fix the image, relax the policy, or deploy through an explicit admin-approved bypass on the stack surface. Blueprints do not silently bypass deploy enforcement.

### Compose content rejected

Sencho accepts valid YAML up to 96 KiB. Split very large compose files into smaller stacks or move generated content out of the Blueprint. Do not paste secrets into the compose body; use environment files or fleet secrets where appropriate.

### Remote node disconnected during apply

The row moves to failed with the remote error. Reconnect the node, verify whether the stack directory contains `docker-compose.yml` and `.blueprint.json`, then click **Apply now**. If the directory exists without a matching marker, Sencho treats it as a name conflict until you rename or remove the remote stack manually.

### Docker daemon or registry failure during apply

The deployment row moves to failed and records the Docker or registry error. Resolve the daemon, socket, registry credentials, rate limit, disk, or volume-permission issue on the affected node, then click **Apply now**. Watch that node's stack activity and security scan status after retry.

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

### Where is my data after "Snapshot, then evict"?

The named volumes managed by the stack on the target node are removed when the eviction runs `docker compose down`. The snapshot in Fleet → Snapshots holds the compose definition only; volume bytes are not included. To preserve data, capture the volume by hand before withdrawing (see *Migrating stateful data between nodes*). Bind mounts on the host filesystem are left in place by both eviction modes.

### "Failed to capture compose snapshot before eviction"

Sencho aborted the eviction because the pre-eviction compose snapshot could not be written. The deployment is still in place. Check the database is reachable (the snapshot lives in `fleet_snapshots`), then retry. If you accept data loss and want to evict regardless, use **Evict and destroy data** instead.

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

## Rollback and watch plan

To roll back a Blueprint hardening change, revert the application build, restart the Sencho backend and frontend, and re-paste the prior compose revision into any affected Blueprint. No database migration rollback is required for the validation and reconciliation guards described here.

For the first 24 to 48 hours after deployment, watch:

- Backend logs tagged `[BlueprintReconciler]`, `[BlueprintService]`, and `[BlueprintService:diag]` when Developer Mode is on.
- Deployment-row counts by node: `failed`, `pending_state_review`, `name_conflict`, and `drifted`.
- Vulnerability policy blocks and post-deploy scan failures on each node.
- Docker daemon errors, registry timeouts, image pull failures, volume permission errors, and out-of-disk errors on the affected node.

Rollback if failed Blueprint deploys exceed 5 percent of apply attempts for 30 minutes, if a single node repeatedly fails all Blueprint deploys after Docker recovers, or if stateful deployments leave `pending_state_review` without an operator action path.

In a fleet, roll back the controlling instance first so it stops issuing new Blueprint actions. Then roll back remote nodes. During version skew, older remote nodes can still receive stack deploy requests, but they may lack matching validation or diagnostic logs. Inspect per-node logs rather than relying only on aggregate fleet counts.
