mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 18:32:52 +00:00
docs: tutorials batch 1 (#1656)
* docs: scaffold Tutorials tab and write enroll-a-remote-node Adds the Tutorials tab to docs.json with 15 stub pages across three groups (Fleet & nodes, Deploy & automate, Secure & integrate), and writes the first full tutorial: enrolling a remote node via Pilot Agent mode, verified end to end against a live control instance and a second host running an existing Jellyfin Compose stack. * docs: write Schedule an Operation tutorial * docs: fix MDX parse error in Schedule an Operation tutorial * docs: write Set Up SSO with Custom OIDC tutorial Registers an OAuth client in a self-hosted identity provider (Keycloak worked example), configures Sencho's Custom OIDC settings, tests the connection, and verifies a real end-to-end login with auto-provisioning from two independent surfaces. * docs: drop unused SSO tutorial screenshot sso-settings-empty.png isn't referenced by the tutorial content. * docs: write Set Up Fleet Federation tutorial Migrates a Blueprint-managed workload from one node to another using pin and cordon, with the confirm-before-mutate rollout in between. Corrects the published feature page's claim that pin requires the global admin role; the code gates cordon and pin identically, scoped to the target node. * docs: write Create and Approve a Blueprint tutorial Covers labeling a target node, authoring a stateless Blueprint, walking through the create-then-approve rollout flow, verifying from the Deployments tab and the audit log, and recovering from a port-conflict deploy failure. Cross-links with Move a Blueprint Deployment to a New Node in both directions. * docs: write Automatically Patch a Stack With an Auto-Update Label tutorial * docs: write Configure Auto-Heal Policies tutorial Adds the full step-by-step content for the Configure Auto-Heal Policies stub: an nginx+redis scenario stack, adding a service-scoped policy, and a live verification that breaks a container's healthcheck, confirms the policy restarts it, and recovers it. * docs: write Set Up Deploy Enforcement tutorial Covers configuring a block-on-deploy scan policy against a stack running a deliberately outdated nginx image, reading the block dialog, and overriding it as an admin with the bypass confirmed in the audit log. Includes a stack-pattern mismatch as the most likely first-time failure. * docs: write Configure Environment Guardrails tutorial Covers the Block deploy on missing required env vars guardrail end to end: deploy a Postgres stack with a required password, enable the guardrail, watch a real update get refused with a named-variable message, fix it, and verify from the Activity and Environment tabs. * docs: write Deploy a Stack Automatically From Your CI Pipeline tutorial * docs: write Catch and Fix a Container That's Drifted From Its Compose File tutorial Covers reading a real Drift finding after an out-of-band container change and resolving it by redeploying through Sencho. * docs: write Connect a Git Source tutorial * docs: write Push a Shared Environment File to Every Node tutorial Writes the Fleet Secrets tutorial: create a bundle, target nodes by label, read the push preview/results, verify via the audit log, and recover from a stack-name typo. Removes the three unwritten placeholder stubs (RBAC, Sencho Mesh, private registries) that had no scheduled content.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: Automatically Restart a Container When It Goes Unhealthy
|
||||
sidebarTitle: Auto-restart an unhealthy container
|
||||
description: Add an Auto-Heal policy that restarts a container when its healthcheck fails or it crashes, then prove it fires against a real failure.
|
||||
---
|
||||
|
||||
Say a small internal service starts failing its healthcheck at 3 AM: a dependency hiccups, a worker wedges, whatever the cause. Nobody wants to be paged for a problem a restart would fix. This walks through adding an Auto-Heal policy to a two-service stack, `ops-status` (an nginx `web` service in front of a `redis` cache), so that if `web` stays unhealthy past a threshold you set, Sencho restarts it on its own, then confirms the policy actually fired by breaking the healthcheck for real and watching the restart happen.
|
||||
|
||||
This tutorial covers one Auto-Heal policy scoped to a single Compose service. It does not cover alert rules (a related but separate tab in the same sheet), stack-wide **All services** policies, or the crash-based healing path (no healthcheck required, triggered by a non-zero exit instead). See the [Auto-Heal Policies](/features/auto-heal-policies) feature page for the full picture, including the four safety rails and the multi-node behavior.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An account with edit access to this stack (admin by default, or a custom role granted `stack:edit` for this stack). Viewing existing policies is open to every signed-in role.
|
||||
- A running stack with at least one service that declares a Docker `HEALTHCHECK`. Auto-Heal's healthcheck-based path only sees containers that report a health status; a service with no `HEALTHCHECK` block never goes `unhealthy`, no matter how broken it is.
|
||||
- This tutorial uses a small stack called `ops-status`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
image: nginx:alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8092:80"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost/ || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
cache:
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
`web` has the healthcheck; `cache` doesn't need one for this tutorial. Deploy this stack (or adapt an existing one with a `HEALTHCHECK` block) before continuing.
|
||||
|
||||
<Note>
|
||||
Auto-Heal restarts the container in place; it does not recreate it. If the thing making a container unhealthy lives in its writable layer (a moved file, a corrupted local state), a restart alone won't fix it and the container will go unhealthy again on the next check. This matters for the verification step below.
|
||||
</Note>
|
||||
|
||||
<Steps>
|
||||
<Step title="Open the stack's Monitor sheet on the Auto-heal tab">
|
||||
Right-click the `ops-status` stack in the sidebar (or focus it and press **H**) and select **Auto-Heal** in the **Inspect** group. The Monitor sheet opens directly on the **Auto-heal** tab, with **Active policies** showing `No auto-heal policies configured for this stack.`
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-auto-heal-policies/sidebar-context-menu-auto-heal.png" alt="The ops-status stack's sidebar context menu with Alerts and Auto-Heal listed under Inspect, Auto-Heal highlighted with its H shortcut." />
|
||||
</Frame>
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-auto-heal-policies/monitor-sheet-empty.png" alt="The Stack ops-status Monitor sheet on the Auto-heal tab, showing no active policies and the empty Add new policy form with Service, Unhealthy for, Cooldown, Max restarts per hr, and Auto-disable after fields." />
|
||||
</Frame>
|
||||
</Step>
|
||||
<Step title="Scope the policy to the web service and set thresholds">
|
||||
In **Add new policy**, open the **Service** combobox and pick **web** instead of the default **All services**, since this policy should only watch the service that has a healthcheck. Set **Unhealthy for (minutes)** to `1` and **Cooldown (minutes)** to `1` so you don't have to wait long to see it fire; leave **Max restarts / hr** at `3` and **Auto-disable after (failures)** at `5`.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-auto-heal-policies/add-policy-form-filled.png" alt="The Add new policy form filled in: Service web, Unhealthy for 1, Cooldown 1, Max restarts per hr 3, Auto-disable after failures 5." />
|
||||
</Frame>
|
||||
|
||||
In production you'd typically set **Unhealthy for** higher (5 minutes or more) so a brief blip doesn't trigger a restart. The 1-minute value here is only to make the next step observable without a long wait.
|
||||
</Step>
|
||||
<Step title="Add the policy and confirm it's active">
|
||||
Click **Add Policy**. A toast confirms `Policy added.`, and the policy now appears in **Active policies**: `web` with the summary `Unhealthy for 1 min · Cooldown: 1 min · Max 3/hr` and its **ON** toggle already enabled.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-auto-heal-policies/active-policy-web.png" alt="Active policies showing one policy for the web service, Unhealthy for 1 min, Cooldown 1 min, Max 3 per hr, with the ON toggle enabled, a history chevron, and a delete icon." />
|
||||
</Frame>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verify it worked
|
||||
|
||||
The policy is saved and enabled, but that alone doesn't prove it fires. Auto-Heal evaluates every 30 seconds in the background, and there's no manual "run now" button, so the only real proof is causing an actual failure and watching Sencho react to it.
|
||||
|
||||
If you have shell access to the host running this stack, break the `web` container's healthcheck on purpose:
|
||||
|
||||
```bash
|
||||
docker exec ops-status-web-1 mv /usr/share/nginx/html/index.html /usr/share/nginx/html/index.html.bak
|
||||
```
|
||||
|
||||
Nginx now returns an error for every request, so the healthcheck's `wget --spider` fails. Within about a minute, the stack header and the `web` container both flip to **unhealthy**:
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-auto-heal-policies/container-unhealthy-triggered.png" alt="The ops-status stack showing RUNNING UNHEALTHY, with ops-status-web-1 marked unhealthy and nginx forbidden errors in the logs." />
|
||||
</Frame>
|
||||
|
||||
Give it another 30–60 seconds for the next evaluation tick, then reopen the Monitor sheet and expand the policy's history chevron. **Recent activity** shows a **Restarted** entry with the reason `Container unhealthy for 1 minute(s); auto-restarted.`:
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-auto-heal-policies/policy-recent-activity-restarted.png" alt="The web policy's Recent activity panel showing two Restarted entries for ops-status-web-1, each with the reason Container unhealthy for 1 minute(s); auto-restarted." />
|
||||
</Frame>
|
||||
|
||||
Because a restart doesn't recreate the container, the moved file is still missing after the restart, so the container goes unhealthy again and the policy keeps restarting it every cooldown window (you can see two **Restarted** entries above, roughly two minutes apart). Restore the file to let it actually recover:
|
||||
|
||||
```bash
|
||||
docker exec ops-status-web-1 mv /usr/share/nginx/html/index.html.bak /usr/share/nginx/html/index.html
|
||||
```
|
||||
|
||||
The next few healthchecks pass, and the container settles back to **healthy** on its own, with no further restarts needed:
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-auto-heal-policies/container-healthy-recovered.png" alt="The ops-status stack back to RUNNING HEALTHY, with ops-status-web-1 showing healthy status about a minute after restarting." />
|
||||
</Frame>
|
||||
|
||||
Check from a second, independent surface too: open the dashboard's **Stack Restarts (7d)** card. It shows an `ops-status` row tagged **AUTO-HEAL** with a restart count, confirming the restarts came from the policy and not a manual action:
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-auto-heal-policies/dashboard-stack-restarts-autoheal.png" alt="The dashboard's Stack Restarts (7d) card showing an ops-status row with an AUTO-HEAL badge and a restart count of 2, and the Configuration Status card showing Auto-heal policies 1 / 1 active." />
|
||||
</Frame>
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
**The policy never fires, and the container never shows a health word at all.** Compare the container card's status line: a container with a working healthcheck reads `up X minutes · healthy` (or `unhealthy`); a container with no `HEALTHCHECK` declared just reads `up X minutes`, with nothing after it. If your service is missing the health word entirely, Auto-Heal's healthcheck path has nothing to evaluate: add a `healthcheck` block to that service in `docker-compose.yml` (see the [Prerequisites](#prerequisites) snippet above) and redeploy the stack. See [Auto-Heal Policies · Troubleshooting](/features/auto-heal-policies#troubleshooting) for the other ways a policy can fail to fire.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Auto-Heal Policies" icon="heart-pulse" href="/features/auto-heal-policies">
|
||||
The full mechanics: safety rails, stack vs. service scope, multi-node behavior, and notifications.
|
||||
</Card>
|
||||
<Card title="Alerts & Notifications" icon="bell" href="/features/alerts-notifications">
|
||||
Pair a policy with an alert rule on the same Monitor sheet for visibility before a restart even fires.
|
||||
</Card>
|
||||
<Card title="Automatically Patch a Stack With an Auto-Update Label" icon="arrows-rotate" href="/tutorials/configure-auto-update-policies">
|
||||
Another hands-off stack policy: keep images current instead of watching for unhealthy containers.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: Automatically Patch a Stack With an Auto-Update Label
|
||||
sidebarTitle: Auto-patch a stack with a label
|
||||
description: Tag a stack for hands-off patching, schedule a label-driven check, and confirm a real digest rebuild gets applied without you clicking Apply yourself.
|
||||
---
|
||||
|
||||
Say you're running a self-hosted app on a rolling image tag like `latest`. The upstream maintainer regularly rebuilds that tag with security patches, same version, same tag name, new image content, and every rebuild is one you'd apply if you noticed it. Nobody wants to check the **Update** board every day for that. This walks through tagging a stack with a **Stack Label** built for exactly this ("safe to patch unattended"), pointing a schedule at that label instead of at one specific stack, and running it once to watch a real pending rebuild get pulled and recreated.
|
||||
|
||||
The worked example is `jackett`, a small self-hosted indexer proxy pinned to `lscr.io/linuxserver/jackett:latest`. This tutorial does not cover bumping a stack to a *newer semver tag* (that always needs a manual Compose edit first, since a schedule never rewrites an image reference) or building the label itself from Settings instead of inline. See the [Auto-Update Policies](/features/auto-update-policies) feature page for the semver workflow and risk badges, and [Stack Labels](/features/stack-labels) for every other way to create and manage labels.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **An admin or node-admin account.** Assigning a Stack Label and creating a scheduled task both require one of these roles; viewers and deployers can see the result but can't set it up.
|
||||
- **A running stack.** Any stack works, but only a stack whose currently-pinned tag has a real pending **same-tag digest rebuild** actually gets pulled and recreated by the schedule. A stack that's fully up to date, or one pinned to a tag with a *newer* tag available instead, still gets checked on schedule, it just has nothing to apply until you edit its Compose pin (see [Auto-Update Policies · Workflow](/features/auto-update-policies#workflow)).
|
||||
- **The hub (Local) selected as the active node for the scheduling step.** Schedules is a hub-level view and isn't available while a remote node is the active selection; labeling the stack itself works from whichever node it lives on.
|
||||
|
||||
<Steps>
|
||||
<Step title="Spot the pending update and create the label">
|
||||
Open the stack that has an update pending. Its **Anatomy** tab shows an **Update available** panel naming the image and marking it `same-tag digest rebuild`, the same signal the sidebar's dot and the **Update** board also carry for this stack.
|
||||
|
||||
Right-click the stack in the sidebar (or use its kebab menu) and choose **Labels → New label**. Type a name that describes the policy, not the app, since you'll likely tag other stacks with it later: `Auto-update`. Pick a color and click **Create**.
|
||||
|
||||
<Frame caption="The stack's own Update available panel, alongside the sidebar context menu's Labels submenu with the inline New label form filled in.">
|
||||
<img src="/images/tutorials/configure-auto-update-policies/context-menu-new-label-form.png" alt="jackett's Anatomy tab showing an Update available panel for lscr.io/linuxserver/jackett:latest marked 'patch · same-tag digest rebuild', with the stack's right-click context menu open on the Labels submenu showing an inline New label form: a text input reading 'Auto-update', a row of ten color swatches with green selected, and Create and Cancel buttons." />
|
||||
</Frame>
|
||||
|
||||
Creating a label from this menu assigns it to the stack in the same round trip, no separate assignment step.
|
||||
</Step>
|
||||
<Step title="Confirm the sidebar grouped by label">
|
||||
The sidebar now splits into label groups instead of one flat list: an **AUTO-UPDATE** section holding this stack, and an **UNLABELED** section for everything else on the node.
|
||||
|
||||
<Frame caption="The sidebar after the label exists, grouped by label instead of flat.">
|
||||
<img src="/images/tutorials/configure-auto-update-policies/sidebar-auto-update-group.png" alt="Sidebar showing an AUTO-UPDATE group containing jackett, and an UNLABELED group containing keycloak-idp and whoami-edge." />
|
||||
</Frame>
|
||||
|
||||
This is cosmetic confirmation, not the policy itself: the schedule you create next is what actually drives updates.
|
||||
</Step>
|
||||
<Step title="Create a schedule that targets the label">
|
||||
Open **More → Schedules**, click **New Schedule**, and set **Action** to **Auto-update stacks by label** (in the **Updates** group). Fill in:
|
||||
|
||||
- **Name**: `Nightly patch check`
|
||||
- **Stack Label**: type `Auto-update` and pick the suggestion that appears (it shows the live match count: `1 stack · 1 node`)
|
||||
- **Scope**: **Entire fleet**. A stack-label schedule resolves membership at run time, so leaving it fleet-wide means any stack you tag with this label later, on any node, is covered automatically without editing the schedule again.
|
||||
- **Schedule**: leave the default **Daily** at **03:00**
|
||||
|
||||
Below the Stack Label field, **Current matches** previews exactly which stacks this task would act on right now, before you save anything.
|
||||
|
||||
<Frame caption="The New scheduled task modal with the label-driven fields filled in and the live match preview resolved.">
|
||||
<img src="/images/tutorials/configure-auto-update-policies/new-schedule-label-filled.png" alt="New scheduled task modal: Name 'Nightly patch check', Action 'Auto-update stacks by label', Stack Label 'Auto-update', Scope segmented control with Entire fleet selected, a Current matches panel reading '1 stack on 1 node' with 'Local: jackett' listed, and Schedule set to Daily at 03:00 with the cron preview 0 3 * * *." />
|
||||
</Frame>
|
||||
|
||||
Click **Create**. See [Schedule an Operation](/tutorials/schedule-an-operation) for the general mechanics of the Schedules timeline and table if this is your first scheduled task; this tutorial goes straight to running it.
|
||||
</Step>
|
||||
<Step title="Confirm the task, then run it once">
|
||||
Switch to **All tasks**. The new row shows **Auto-update stacks by label** as the action and `Label: Auto-update · Entire fleet` as the target, with **Status** `Never run`.
|
||||
|
||||
<Frame caption="The new label-driven task in the All tasks table before its first run.">
|
||||
<img src="/images/tutorials/configure-auto-update-policies/all-tasks-row.png" alt="All tasks table with one row: Nightly patch check, Auto-update stacks by label, Label: Auto-update · Entire fleet, At 03:00 AM / 0 3 * * *, Status Never run, Next Run 8/6/2026 11:00:00 PM, Enabled ON." />
|
||||
</Frame>
|
||||
|
||||
Click **Run now** (play icon) rather than waiting for 03:00. This is a real pull and recreate, so it takes longer than a restart task, tens of seconds rather than a fraction of one. Click **Refresh** after it finishes.
|
||||
|
||||
<Frame caption="The same row after a successful run: Status flips to Success and the pending update clears.">
|
||||
<img src="/images/tutorials/configure-auto-update-policies/all-tasks-success.png" alt="All tasks table with the Nightly patch check row now showing a green Success badge, and the sidebar's UPDATES filter chip reading 0 instead of 1." />
|
||||
</Frame>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verify it worked
|
||||
|
||||
Check from two places, since a status badge alone can't tell you *what* got updated, and a cleared update alone can't tell you it was this schedule that cleared it.
|
||||
|
||||
**The Execution history.** Click the **Execution history** button (clock icon) on the row. The run shows **Source** `Manual`, **Status** `Success`, and details naming exactly what happened: `Selector: stack-label="Auto-update" · scope=entire fleet` followed by `Stack "jackett": updated (lscr.io/linuxserver/jackett:latest).`
|
||||
|
||||
<Frame caption="Execution history showing the label selector that was resolved and the exact image that got updated.">
|
||||
<img src="/images/tutorials/configure-auto-update-policies/run-history.png" alt="Execution history sheet for Nightly patch check showing one run: Source Manual, Status Success, Duration 24.1s, and details naming the stack-label selector and 'Stack jackett: updated (lscr.io/linuxserver/jackett:latest)'." />
|
||||
</Frame>
|
||||
|
||||
**The Update readiness board.** Open **More → Update**. Where jackett's card used to show `Rebuild available`, the board now reads `Everything is up to date`, and the sidebar's **Updates** filter chip is back to `0`.
|
||||
|
||||
<Frame caption="The Update readiness board after the schedule cleared the only pending rebuild.">
|
||||
<img src="/images/tutorials/configure-auto-update-policies/readiness-board-up-to-date.png" alt="Update readiness board showing the empty state: a shield icon, the headline 'All stacks on current builds', and the subtitle 'Sencho rechecks registries on the configured interval.' The sidebar's UPDATES chip reads 0." />
|
||||
</Frame>
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
**A later run reports success but changes nothing.** Run the task again (or let it fire at 03:00 the next night) once jackett is already current, and the row still shows the green **Success** badge, not a failure. Open its Execution history and the details read `Stack "jackett": all images up to date.` instead of naming an updated image. This is the expected steady state, not a bug: the label schedule checks every stack under the label on every run, and "nothing changed because there was nothing to change" is success, not skipped. It only becomes worth investigating if a stack you know has a real pending digest rebuild keeps reporting "up to date"; in that case, confirm the label is still assigned to that stack (right-click it and check the **Labels** submenu for a checkmark) and that the stack's own image reference still uses the same tag Sencho detected the rebuild against.
|
||||
|
||||
<Frame caption="A second run against an already-current stack: still Success, with a different, unambiguous detail message.">
|
||||
<img src="/images/tutorials/configure-auto-update-policies/run-history-two-runs.png" alt="Execution history sheet showing two runs for Nightly patch check: the newer run at 9:49:09 PM with Status Success, Duration 0.8s, and details 'Stack jackett: all images up to date', and the earlier run at 9:44:20 PM with Duration 24.1s and details naming the updated image." />
|
||||
</Frame>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Auto-Update Policies" icon="arrows-rotate" href="/features/auto-update-policies">
|
||||
The full readiness board reference: risk badges, the semver-bump workflow this tutorial doesn't cover, and multi-node behavior.
|
||||
</Card>
|
||||
<Card title="Stack Labels" icon="tags" href="/features/stack-labels">
|
||||
Every other way to create, assign, and manage labels, plus the label-driven Fleet Actions and mute shortcuts.
|
||||
</Card>
|
||||
<Card title="Scheduled Operations" icon="calendar-clock" href="/features/scheduled-operations">
|
||||
The full action list and cron reference shared by every scheduled task type, including this one.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: Stop a Deploy When a Required Environment Variable Is Missing
|
||||
sidebarTitle: Block a missing required var
|
||||
description: Turn on the deploy guardrail that refuses a stack update when a required environment variable has no value, and watch it catch a real missing password before anything breaks.
|
||||
---
|
||||
|
||||
Say a stack declares a required database password with `${DB_PASSWORD:?message}` in its Compose file. Nothing stops a teammate from clearing that value in `.env` by accident, and a normal `docker compose up` would let the mistake ride until the container fails at runtime. This walks through turning on the guardrail that catches it up front instead: you'll deploy a small Postgres stack with a required password, clear the password to simulate the mistake, watch the next update get refused with a clear message naming the variable, then fix it and confirm the update goes through.
|
||||
|
||||
This tutorial covers one guardrail: **Block deploy on missing required env vars**. It doesn't cover the rest of the Environment inventory (secret classification, duplicate or shell-only detection, project environment file selection) or the other Deploy Guardrails settings (health observation, rollback retention, automatic external network creation); see the [Environment and Secrets Guardrails](/features/environment-guardrails) feature page for the complete picture.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The `admin` or `node-admin` role. Changing the guardrail setting needs `node:manage` permission, which both roles hold; updating the stack needs `stack:deploy`, which every role except `viewer` and `auditor` holds.
|
||||
- Available on every tier; no Admiral requirement.
|
||||
- The setting is scoped to one node. If you manage more than one, turn it on for the node you deploy the scenario stack to.
|
||||
|
||||
<Steps>
|
||||
<Step title="Deploy a stack with a required password">
|
||||
Select **Create Stack**, name it `inventory-db`, and replace its Compose file with:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
db:
|
||||
image: postgres:16
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD in .env before deploying}
|
||||
POSTGRES_DB: inventory
|
||||
ports:
|
||||
- "5432:5432"
|
||||
```
|
||||
|
||||
The `${DB_PASSWORD:?message}` syntax tells Compose the variable is required: it refuses to render the stack at all if `DB_PASSWORD` has no value, anywhere in the file. The **.env** tab stays disabled until a project environment file exists, so switch to the **Files** tab, select **New file**, and create one named `.env`. Open it and add `DB_PASSWORD=devpassword123` before your first deploy. Setting it now gets you a clean first deploy; the next steps remove it on purpose to see the guardrail react.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-environment-guardrails/compose-required-password.png" alt="Compose editor for inventory-db showing a single db service: image postgres:16, restart always, environment with POSTGRES_PASSWORD set to the interpolation ${DB_PASSWORD:?Set DB_PASSWORD in .env before deploying}, POSTGRES_DB inventory, and port 5432 published." />
|
||||
</Frame>
|
||||
|
||||
Select **Save & Deploy**. The container comes up healthy.
|
||||
</Step>
|
||||
<Step title="Confirm the variable in the Environment tab">
|
||||
Open the stack and switch to the **Environment** tab in the Anatomy panel header. `DB_PASSWORD` appears under **present** with **secret** and **required** badges, sourced from `.env` and scoped to interpolation. The lock badge means its value is never read into the inventory, only its presence and status.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-environment-guardrails/env-tab-db-password-required.png" alt="Environment tab for inventory-db showing 3 vars, 2 likely secret, and a present group listing DB_PASSWORD (secret, required, present, source .env, scope interpolation), POSTGRES_DB (present, source inline, scope injected), and POSTGRES_PASSWORD (secret, present, source inline, scope injected)." />
|
||||
</Frame>
|
||||
</Step>
|
||||
<Step title="Turn on the guardrail">
|
||||
Open **Settings** → **Infrastructure** → **Stacks** and find the **Deploy Guardrails** section. Turn on **Block deploy on missing required env vars**, then select **Save settings**. It's off by default: without it, Compose only reports a missing required variable when the deploy or update actually runs.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-environment-guardrails/deploy-guardrails-setting-on.png" alt="Deploy Guardrails settings section with Block deploy on missing required env vars set to ON, alongside Observe health after updates, Observation window, Superseded rollback retention, Maximum retained rollback generations, and Automatically create missing external networks." />
|
||||
</Frame>
|
||||
</Step>
|
||||
<Step title="Clear the password to simulate the mistake">
|
||||
Back on `inventory-db`, open the **.env** tab and clear the value so the line reads `DB_PASSWORD=`. Select **Save & Deploy** and choose **Save Only** from the dropdown next to it, so the change is written to disk without triggering a deploy yet.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-environment-guardrails/env-file-password-cleared.png" alt=".env editor for inventory-db showing a single line, DB_PASSWORD with no value, with the stack still running above." />
|
||||
</Frame>
|
||||
</Step>
|
||||
<Step title="Update the stack and watch the guardrail fire">
|
||||
Select **Update**. A readiness dialog summarizes preflight, drift, current containers, and a few other checks; note that none of them mention environment variables; the dialog can say **ready** even though this update is about to be refused. Select **Update now** anyway.
|
||||
|
||||
The update fails immediately, before any image pull or container change, with **Deploy blocked: required environment variable DB_PASSWORD is missing. Define it in a .env or env_file, then deploy again.** The full message is easiest to read from the notification bell in the top bar.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-environment-guardrails/update-blocked-notification.png" alt="Notifications panel with the top entry reading 'Deploy blocked: required environment variable DB_PASSWORD is missing. Define it in a .env or env_file, then deploy again.', 2 minutes ago, above older entries for a vulnerability scan, a health gate pass, and an image update." />
|
||||
</Frame>
|
||||
</Step>
|
||||
<Step title="Fix it and confirm the update succeeds">
|
||||
Reopen the **.env** tab, set the line back to `DB_PASSWORD=devpassword123`, and save. Select **Update** again. This time it runs to completion: the image pulls, the container recreates, and the health gate passes.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-environment-guardrails/update-succeeded-health-gate.png" alt="Updating inventory-db progress dialog showing Succeeded, a passed health gate message, and a log ending in '=== Stack updated successfully ===' and '=== Pruned dangling images ==='." />
|
||||
</Frame>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verify it worked
|
||||
|
||||
Check from two independent surfaces so you're not trusting a single UI element.
|
||||
|
||||
**The Activity tab.** Open the stack's **Activity** tab. Reading newest first, you'll see the health gate pass and the successful update sitting right above the earlier **Deploy blocked** entry, so the block and the recovery are both on the record with the account that triggered each one.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-environment-guardrails/activity-tab-block-and-fix.png" alt="Activity tab for inventory-db listing, newest first: health gate passed, stack updated, update started, Deploy blocked: required environment variable DB_PASSWORD is missing, an earlier vulnerability scan and health gate pass, and the original deploy." />
|
||||
</Frame>
|
||||
|
||||
**The Environment tab.** Reopen it: `DB_PASSWORD` is back under **present** with its **required** badge, and the summary line reads 0 missing.
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
**The variable still shows missing after you set it, but in the wrong place.** Compose's `${VAR:?message}` interpolation only resolves from the project `.env` file (or the shell), never from a service's `env_file:` entries: those are only injected into the container after interpolation already ran. Moving `DB_PASSWORD` into a separate `env_file:`-declared file instead of the project `.env` reproduces the exact same block, because as far as interpolation is concerned the variable is still unset.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/configure-environment-guardrails/compose-env-file-mistake.png" alt="Compose editor for inventory-db showing env_file: - secrets.env added under the db service, with POSTGRES_PASSWORD still set to the ${DB_PASSWORD:?...} interpolation." />
|
||||
</Frame>
|
||||
|
||||
Move the value back into the project `.env` file (or add it there in addition to the `env_file`) and redeploy; the guardrail clears as soon as interpolation can see it. See [Environment and Secrets Guardrails](/features/environment-guardrails#interpolation-versus-container-injection) for the full interpolation-versus-injection explanation.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Environment and Secrets Guardrails" icon="triangle-exclamation" href="/features/environment-guardrails">
|
||||
The full inventory: secret classification, env file status, and project environment file selection.
|
||||
</Card>
|
||||
<Card title="Compose Doctor" icon="stethoscope" href="/features/compose-doctor">
|
||||
Pre-deploy preflight that also surfaces a missing `env_file:` as a high-risk finding.
|
||||
</Card>
|
||||
<Card title="Fleet Secrets" icon="lock" href="/features/fleet-secrets">
|
||||
Author encrypted env-var bundles on the hub and push them to labeled stacks across the fleet.
|
||||
</Card>
|
||||
<Card title="Health-Gated Updates" icon="shield-check" href="/features/health-gated-updates">
|
||||
The post-deploy health observation configured in the same Deploy Guardrails settings section.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Configure Recovery Vault Backups
|
||||
sidebarTitle: Configure Recovery Vault backups
|
||||
description: Step-by-step instructions for configuring and restoring fleet backups with Recovery Vault.
|
||||
---
|
||||
|
||||
<Note>
|
||||
This tutorial is a placeholder pending full content. See the [Fleet Backups feature page](/features/fleet-backups) for the current reference documentation.
|
||||
</Note>
|
||||
|
||||
## What you'll do
|
||||
|
||||
## Prerequisites
|
||||
|
||||
<Steps>
|
||||
<Step title="">
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Fleet Backups" icon="shield-halved" href="/features/fleet-backups">
|
||||
Configure Recovery Vault backups and restore stacks or nodes.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: Deploy Compose Changes by Pulling From Git Instead of Editing by Hand
|
||||
sidebarTitle: Deploy compose changes from Git
|
||||
description: Link a running stack to a Git repository, pull a real commit, review the diff, and deploy it, instead of hand-editing the compose file in the browser.
|
||||
---
|
||||
|
||||
Say `marketing-site` is a small nginx stack you created directly in Sencho, and your team has decided the compose file should live in a Git repository instead, so changes go through a commit and a pull request before they reach the stack. This walks through connecting that already-running stack to a repository, pulling a real commit a teammate pushed, reviewing the diff Sencho builds against what is on disk, and applying it, which both writes the new file and redeploys the container.
|
||||
|
||||
This tutorial covers linking an existing stack to a Git source and running one manual pull-review-apply cycle. It doesn't cover creating a brand-new stack directly from a repository, the three webhook-driven apply modes, or multi-file compose sources; see the [Git Sources](/features/git-sources) feature page for all of that.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The `admin` or `node-admin` role. Linking a source and running a pull or apply needs the `stack:edit` permission, which `deployer`, `viewer`, and `auditor` do not hold.
|
||||
- A running stack to link. If you don't have one, select **Create Stack**, name it `marketing-site`, and replace its compose file with:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.27-alpine
|
||||
restart: always
|
||||
ports:
|
||||
- "8098:80"
|
||||
```
|
||||
|
||||
Select **Save & Deploy**. This tutorial's steps and screenshots use this exact stack, so if you use a different name or port, adjust accordingly.
|
||||
- A public Git repository over HTTPS that you control, with a compose file committed at its root. The screenshots below use a small throwaway demo repository; any repository with the same shape works.
|
||||
- Available on every tier, including Community; no Admiral requirement.
|
||||
|
||||
<Steps>
|
||||
<Step title="Open the Git Source panel">
|
||||
Open the `marketing-site` stack and select **Git Source** in the editor toolbar. The panel opens empty, since nothing is linked yet.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/connect-a-git-source/git-source-panel-empty.png" alt="Empty Git source panel for the marketing-site stack, showing blank Repository URL and Branch fields defaulted to main, a compose.yaml entry marked primary in Compose files, Public (no auth) selected under Authentication, and Review only selected under Apply behavior." />
|
||||
</Frame>
|
||||
</Step>
|
||||
<Step title="Point it at your repository">
|
||||
Paste your repository's HTTPS URL into **Repository URL** (for example `https://github.com/your-org/your-repo.git`). Leave **Branch** on its default, `main`, unless your repository uses a different one.
|
||||
|
||||
Select **Browse** to confirm Sencho can actually reach the repository and see its files, rather than trusting the URL is correct. The browser lists every file in the repository; `compose.yaml` is already ticked as the primary compose file, since that name matches the picker's default.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/connect-a-git-source/browse-picker.png" alt="Git source panel with a Repository URL filled in and the Browse file picker open below the Compose files list, showing a checked compose.yaml and an unchecked README.md fetched live from the repository." />
|
||||
</Frame>
|
||||
|
||||
Leave **Authentication** on **Public (no auth)** for a public repository, and **Apply behavior** on **Review only**, the safest default: a pull only stages a diff for you to review, it never writes or deploys on its own. Select **Save**. Sencho runs a reachability check against the repository before persisting anything; if that check fails, nothing is saved and the panel reports why.
|
||||
</Step>
|
||||
<Step title="Pull the latest commit">
|
||||
Now make a change the way your team actually would: edit the compose file in your repository (not in Sencho) and push a commit. For this tutorial, bump the pinned tag from `nginx:1.27-alpine` to `nginx:1.28-alpine` and push it to the branch you configured.
|
||||
|
||||
Back in the Git Source panel, select **Pull now**. Sencho fetches the branch's current commit and opens a side-by-side diff against what's on disk.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/connect-a-git-source/pull-preview-diff.png" alt="GIT · PULL PREVIEW dialog for marketing-site, showing a Monaco side-by-side diff with the on-disk compose.yaml on the left and the incoming commit on the right, the only difference highlighted on the image line: nginx:1.27-alpine changing to nginx:1.28-alpine." />
|
||||
</Frame>
|
||||
|
||||
The diff shows only the line that actually changed. If the incoming compose file failed `docker compose config` validation, an error banner would appear here and the **Apply** button would stay disabled; since this pull is clean, Apply is enabled.
|
||||
</Step>
|
||||
<Step title="Apply and deploy in one step">
|
||||
Tick **Deploy after apply** at the bottom of the dialog, then select **Apply**. This both writes the incoming file to disk and runs `docker compose up -d` against it, so the running container picks up the new tag immediately instead of just staging the file for a later manual deploy.
|
||||
|
||||
Sencho shows the deploy's progress the same way any other deploy does, ending in a health check on the recreated container.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verify it worked
|
||||
|
||||
Check from two places, since neither alone proves the pull was actually applied and deployed.
|
||||
|
||||
**The Git Source panel.** Reopen it. The **Last applied commit** stat strip at the bottom now shows the short SHA of the commit you just pulled, with an updated timestamp.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/connect-a-git-source/last-applied-commit.png" alt="Git source panel for marketing-site with Apply behavior still on Review only, and a Last applied commit stat strip at the bottom showing a short commit SHA and a just-now timestamp." />
|
||||
</Frame>
|
||||
|
||||
**The stack itself.** Reopen `marketing-site`. The container recreated on the new image: a fresh uptime, and its logs now start with `nginx/1.28.3` instead of `1.27.5`. The compose editor tab reflects the same change; if it still shows the old tag, reload the page, since the editor buffer doesn't refresh itself after a Git-driven write.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/connect-a-git-source/stack-redeployed.png" alt="marketing-site stack page after redeploy, showing marketing-site-web-1 up about a minute, logs starting from nginx/1.28.3, and the compose editor showing image nginx:1.28-alpine." />
|
||||
</Frame>
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
**Saving the source fails with "Repository not found or not accessible," even though the URL is right.** Branch names are case-sensitive, and a typo there (`Main` instead of `main`, for example) surfaces as this same repository-level error rather than a distinct branch error, since Sencho can't always tell a missing branch apart from a missing repository during the reachability check. Double-check the branch name against what your Git host actually shows before assuming the URL itself is wrong.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/connect-a-git-source/branch-error.png" alt="Git source panel with Branch set to the incorrect value Main, and a red error toast in the corner reading Repository not found or not accessible." />
|
||||
</Frame>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Git Sources" icon="code-branch" href="/features/git-sources">
|
||||
Multi-file sources, the three webhook apply modes, private-repo authentication, and the full troubleshooting table.
|
||||
</Card>
|
||||
<Card title="Deploy a Stack Automatically From Your CI Pipeline" icon="satellite-dish" href="/tutorials/set-up-a-webhook">
|
||||
Trigger a Git source pull and deploy from CI instead of pulling by hand from the panel.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
title: Create and Approve a Blueprint
|
||||
sidebarTitle: Create and approve a blueprint
|
||||
description: Author a fleet-wide compose template, target it at a node with a label, and walk through the confirm-before-mutate rollout that puts it on the fleet.
|
||||
---
|
||||
|
||||
Every other lane in Sencho manages one Compose file on one node. A **Blueprint** is the other lane: you declare a Compose file once, point it at the nodes you want by label or by ID, and Sencho keeps those nodes in sync with that declaration going forward, deploying it where it's missing and telling you the moment a targeted node drifts from what you declared. This walks through the two-part motion every Blueprint goes through before it touches your fleet: you **create** it (author the compose, pick a target, choose how drift gets handled), then you **approve** it (review exactly what Sencho is about to place, and confirm). By the end, a small stateless service is running on a real node, deployed entirely through the Blueprint you approved rather than a manual `docker compose up`.
|
||||
|
||||
This tutorial does not cover stateful Blueprints (the extra confirmation step for named volumes and bind mounts), pinning a Blueprint to a specific node, or moving a deployment between nodes. See the [Blueprints](/features/blueprint-model) feature page for stateful workflows, and [Move a Blueprint Deployment to a New Node](/tutorials/set-up-fleet-federation) once you've completed this one and want to relocate a deployment with Federation.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **An admin account.** Creating, editing, and applying a Blueprint all require the admin role. Operators and viewers can read the catalog and the detail sheet but can't act on either.
|
||||
- **At least one node**, local or remote, that you can attach a label to. Blueprints are a Community-tier capability; no paid plan is required.
|
||||
|
||||
<Steps>
|
||||
<Step title="Label the node you're targeting">
|
||||
Blueprints target nodes by label or by explicit node ID. Open **Settings → Infrastructure → Nodes**, find the node you want this Blueprint to reach, and click the **+** button in its **Labels** column. Type `edge` and click **Add**.
|
||||
|
||||
<Frame caption="Adding the edge label to a node from Settings · Infrastructure · Nodes.">
|
||||
<img src="/images/tutorials/create-and-approve-a-blueprint/node-labels-add-edge.png" alt="Add label popover open on a node's row in the Nodes settings table, with 'edge' typed into the input and an Add button." />
|
||||
</Frame>
|
||||
|
||||
Labels are plain lowercase strings, not key/value pairs. You can add more than one to a node, and a Blueprint's selector can match on any combination of them.
|
||||
</Step>
|
||||
<Step title="Author the Blueprint">
|
||||
Go to **Fleet → Deployments** and click **New Blueprint**. Fill in:
|
||||
|
||||
- **Name**: `whoami-edge`
|
||||
- **Description**: `Minimal identity endpoint for the edge tier`
|
||||
|
||||
In the **Compose** editor, replace the placeholder YAML with a small, stateless HTTP service that just echoes back which container answered, useful for confirming a deploy actually landed:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
image: traefik/whoami:v1.10
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:80"
|
||||
```
|
||||
|
||||
As you type, the classification banner above the editor updates. With no volumes in this compose, it settles on **Stateless · portable**: Sencho can deploy and evict this Blueprint freely, with none of the extra confirmation prompts a stateful Blueprint requires.
|
||||
|
||||
<Frame caption="Name, description, and compose filled in. The classification banner reads Stateless · portable.">
|
||||
<img src="/images/tutorials/create-and-approve-a-blueprint/blueprint-editor-compose.png" alt="Blueprint editor dialog with Name whoami-edge, Description 'Minimal identity endpoint for the edge tier', and a compose YAML editor showing the traefik/whoami service. The classification banner above the editor reads Stateless · portable, no persistent volumes detected." />
|
||||
</Frame>
|
||||
|
||||
Scroll down to **Selector** and leave **Labels** selected. Click the `edge` pill under **Match nodes with ANY of these labels**. The line below confirms `Resolves to nodes labelled any of [edge]`. Under **Drift policy**, leave **Suggest** selected, the middle option: Sencho will always detect drift on this Blueprint, and Suggest additionally sends a notification when it happens, without redeploying anything on its own. (**Observe** stays silent about drift beyond the deployment row; **Enforce** auto-redeploys to fix it.)
|
||||
|
||||
<Frame caption="Selector set to the edge label, Suggest drift policy selected.">
|
||||
<img src="/images/tutorials/create-and-approve-a-blueprint/blueprint-editor-selector-drift.png" alt="Blueprint editor scrolled to the Selector and Drift policy sections. The edge label pill is selected under 'Match nodes with ANY of these labels', with the resolves-to summary line beneath it. The Suggest drift policy card is selected among Observe, Suggest, and Enforce." />
|
||||
</Frame>
|
||||
|
||||
Click **Create blueprint**.
|
||||
</Step>
|
||||
<Step title="See the blueprint land in pending state">
|
||||
The detail sheet opens on your new Blueprint: `any=[edge] · suggest · rev 1`, with **Apply now**, **Edit**, **Disable**, and **Delete** across the top. The **Deployments** section is still empty, with the note `No matching nodes yet. Add a label or pick a node ID, then click Apply now.`
|
||||
|
||||
<Frame caption="The Blueprint immediately after creation. No deployment row exists yet, even though the label already matches a real node.">
|
||||
<img src="/images/tutorials/create-and-approve-a-blueprint/blueprint-created-pending.png" alt="Blueprint detail sheet for whoami-edge right after creation, showing the Apply now, Edit, Disable, and Delete actions, the description, and an empty Deployments section reading 'No matching nodes yet. Add a label or pick a node ID, then click Apply now.' The footer reads Updated just now, pending." />
|
||||
</Frame>
|
||||
|
||||
This is expected, not a bug: creating a Blueprint only declares intent. Sencho computes what it would actually do, and does it, only once you open the rollout preview.
|
||||
</Step>
|
||||
<Step title="Review and confirm the rollout">
|
||||
Click **Apply now**. Sencho computes a rollout preview: `Safe 1 · Warnings 1 · Blockers 0`, with a warning noting `no persistent volumes detected` (informational for a stateless Blueprint, not a problem), and one line under **Changes**: `Local (local/online) · create · safe: New placement`.
|
||||
|
||||
<Frame caption="The rollout preview for the first deploy: one safe create, one informational warning.">
|
||||
<img src="/images/tutorials/create-and-approve-a-blueprint/rollout-preview.png" alt="Confirm rollout dialog for whoami-edge reading Safe 1, Warnings 1, Blockers 0, pending. A Warnings section lists 'no persistent volumes detected'. A Changes section lists Local (local/online) · create · safe: New placement." />
|
||||
</Frame>
|
||||
|
||||
This preview is the actual authorization step: nothing on the fleet has changed yet, and Sencho is showing you exactly what it's about to do before it does it. Click **Confirm Apply**.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verify it worked
|
||||
|
||||
Check from two places, since a single stale UI element could tell you the wrong thing.
|
||||
|
||||
**The Deployments tab.** Back on **Fleet → Deployments**, the `whoami-edge` tile now shows a green dot, the **stateless** chip, and `1/1 active · any=[edge]`. Open the tile: the deployment table lists **Local**, status **Active**, with a **Withdraw** action now available. The stack also appears in the left sidebar's stack list like any other running stack, materialized on disk the same way a per-stack deploy would be.
|
||||
|
||||
<Frame caption="The Deployments tab after the rollout: one active deployment, stateless, matched on the edge label.">
|
||||
<img src="/images/tutorials/create-and-approve-a-blueprint/catalog-active.png" alt="Deployments · Blueprints catalog with one tile: whoami-edge, stateless, 1/1 active, any=[edge], drift suggest. The left sidebar stack list now includes whoami-edge alongside the node's other stacks." />
|
||||
</Frame>
|
||||
|
||||
<Frame caption="The detail sheet's deployment table: Local, Active, with Withdraw available.">
|
||||
<img src="/images/tutorials/create-and-approve-a-blueprint/detail-sheet-active.png" alt="Blueprint detail sheet for whoami-edge with the deployment table showing one row: Local, status Active, last activity 1m ago, and a Withdraw action." />
|
||||
</Frame>
|
||||
|
||||
**The audit log.** Open **Audit**, switch to **Table**, and search `blueprints/1` (or whatever ID your Blueprint got, visible in the browser's address bar or in the raw preview response). You'll see a `POST /api/blueprints/1/apply` row for each time you clicked **Confirm Apply**, each logged with your account, a timestamp, and a 200 status. If you also hit and fixed a port conflict (see below), a `PUT /api/blueprints/1` row from saving the edit shows up between the two applies.
|
||||
|
||||
<Frame caption="Audit log filtered to this Blueprint's requests: the first apply, the compose edit, and the second apply after the fix.">
|
||||
<img src="/images/tutorials/create-and-approve-a-blueprint/audit-log-blueprint-actions.png" alt="Audit log table filtered by search term blueprints/1, showing two POST /api/blueprints/1/apply rows and one PUT /api/blueprints/1 row, each with status 200 and node 1." />
|
||||
</Frame>
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
The most common first-time failure is a host port that's already taken. `8080` is a common default that other containers on the same node may already be bound to. If that happens, the deployment row moves to **Failed**, and the notes column carries the real Docker error, ending in something like `Bind for 0.0.0.0:8080 failed: port is already allocated`.
|
||||
|
||||
<Frame caption="A failed first deploy: the notes column carries the exact daemon error, including the port conflict.">
|
||||
<img src="/images/tutorials/create-and-approve-a-blueprint/deployment-failed-port-conflict.png" alt="Blueprint deployment row for Local in Failed status, with a Retry action, and a notes column showing the full container startup log ending in 'Bind for 0.0.0.0:8080 failed: port is already allocated'." />
|
||||
</Frame>
|
||||
|
||||
To recover, click **Edit**, change the host-side port in the `ports` mapping to something free (`8091:80`, for instance), and **Save changes**. Saving bumps the revision and clears approval back to pending, so click **Apply now** again: the preview this time shows an `update` action instead of `create`. Confirm it, and the deployment moves through **Deploying** to **Active** on the new port.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Blueprint Model" icon="drafting-compass" href="/features/blueprint-model">
|
||||
The full mental model: markers, drift modes, stateful safety rails, and the reconciler's approval mechanics.
|
||||
</Card>
|
||||
<Card title="Move a Blueprint Deployment to a New Node" icon="network-wired" href="/tutorials/set-up-fleet-federation">
|
||||
Once a Blueprint is running, use Federation's pin and cordon controls to relocate it to a different node.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Enroll a Remote Node
|
||||
sidebarTitle: Enroll a remote node
|
||||
description: Bring a second server under management in the same Sencho console, without opening any inbound ports on it.
|
||||
---
|
||||
|
||||
Say you run Sencho on one server already, and you separately have a home media server running a Jellyfin stack via Docker Compose, on a connection with no port forwarding. This walks through bringing that second server into your Sencho console as a remote node, using **Pilot Agent** mode: the remote host only needs outbound HTTPS, so nothing has to be opened on your router or firewall.
|
||||
|
||||
By the end, both servers appear side by side in the same console, and Sencho can see the Jellyfin containers already running on the second one.
|
||||
|
||||
This tutorial does not cover **Distributed API Proxy** mode (for a remote host you already expose on a stable URL), [Sencho Mesh](/features/sencho-mesh) cross-node networking, or adopting a discovered Compose file into a managed stack. See the [Multi-Node Management](/features/multi-node) feature page for those.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An **admin** account on the Sencho instance that will act as the control instance. Adding, editing, and testing nodes requires admin.
|
||||
- Docker and the Docker Compose plugin installed on the second server, with SSH or console access as the user that owns the Docker socket.
|
||||
- The second server must be able to reach your control instance over outbound HTTPS (or HTTP, if both are on the same private network or VPN).
|
||||
|
||||
<Warning>
|
||||
If your control instance is not reachable from the public internet or the remote server's network (for example, it only listens on `localhost` with no port forwarding and no `SENCHO_PUBLIC_URL` set), the agent will never dial home and the node will sit at `tunnel (waiting)` indefinitely. Confirm the remote host can reach your control instance's URL before starting.
|
||||
</Warning>
|
||||
|
||||
<Steps>
|
||||
<Step title="Open the Add node form">
|
||||
On your control instance, click your avatar in the top-right and choose **Settings**. In the sidebar, under **Infrastructure**, pick **Nodes**, then click **Add node**.
|
||||
|
||||
Set **Name** to something that identifies the host (`media-server` here). Leave **Type** as **Remote** and **Mode** as **Pilot Agent**, both defaults. In **Compose Directory**, enter the absolute path on the remote host where its Compose stacks already live, or where you want them to live, `/home/ubuntu/docker` in this example. The agent mounts that same path inside its own container, so existing bind-mount paths in your Compose files keep working unchanged.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/enroll-a-remote-node/add-node-filled.png" alt="Add remote node dialog with Name set to media-server, Type set to Remote, Mode set to Pilot Agent, and Compose Directory set to /home/ubuntu/docker." />
|
||||
</Frame>
|
||||
|
||||
Click **Add node**.
|
||||
</Step>
|
||||
<Step title="Deploy the agent on the remote host">
|
||||
A dialog opens with a generated Compose file and a start command.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/enroll-a-remote-node/pilot-enroll-modal.png" alt="Enroll the pilot agent dialog. Step 1 shows a generated compose.yaml with SENCHO_MODE set to pilot, SENCHO_PRIMARY_URL pointing at the control instance, and a SENCHO_ENROLL_TOKEN. Step 2 shows the docker compose up -d start command. The footer reads Expires 14m from now, with a Copy compose file button." />
|
||||
</Frame>
|
||||
|
||||
Save Step 1's contents as `compose.yaml` in a directory on the remote host (it does not need to be the Compose Directory itself), then run Step 2's command in that same directory as the user that owns Docker:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
This pulls the `saelix/sencho:latest` image, mounts the host's Docker socket, and starts a container named `sencho-agent` that dials out to your control instance. It does not touch or restart anything already running on the host, including your existing Jellyfin container.
|
||||
|
||||
The enrollment token embedded in the file is valid for **15 minutes and can only be used once**. If it expires before you run the command, see [If something goes wrong](#if-something-goes-wrong) below.
|
||||
</Step>
|
||||
<Step title="Verify the tunnel connects">
|
||||
Back on the Nodes table, the new row starts at **Status: Unknown** with Endpoint `tunnel (waiting)`. Once the agent dials home, usually within a few seconds of `docker compose up -d` finishing, the row flips to **Status: Online** and the Endpoint reads `tunnel (seen Xs ago)`.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/enroll-a-remote-node/nodes-table-online.png" alt="Nodes table with two rows: Local showing Online and docker.sock, and media-server showing Remote, Pilot Agent, tunnel (seen 2m ago), and Online." />
|
||||
</Frame>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verify it worked
|
||||
|
||||
Check from two places, since either alone could be misleading (a stuck browser tab, or a table row that has not refreshed).
|
||||
|
||||
**The node switcher.** Click the switcher at the top of the sidebar. Your new node appears in the **Connected** list with an `AGENT` kicker and a `SEEN Xm AGO` timestamp.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/enroll-a-remote-node/node-switcher-popover.png" alt="Node switcher popover listing 2 connected nodes: Local with a star icon, and media-server with an AGENT kicker and SEEN 2M AGO." />
|
||||
</Frame>
|
||||
|
||||
**The Fleet view.** Switch back to Local if you navigated away, then open the **Fleet** tab. Both nodes show as separate cards with independent CPU, RAM, and disk stats pulled live from each host. The media-server card's running-container count reflects Jellyfin (and the agent itself), not just an empty shell.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/enroll-a-remote-node/fleet-view-two-nodes.png" alt="Fleet overview with two cards: Local showing 1 running container, and media-server showing 3 running containers, 0 stopped, with independent CPU, RAM, and disk gauges." />
|
||||
</Frame>
|
||||
|
||||
Click the node switcher and select your new node to confirm you can operate against it directly. Every top-level view scoped to that host, Home, Resources, Networking, Security, works the same way it does for Local. Fleet-wide views (Fleet itself, Schedules, Audit, Logs, Auto-Update) stay hidden until you switch back to Local, because they only make sense from the hub.
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
The most common first-time snag is the 15-minute enrollment token expiring before you run `docker compose up -d`, usually because of a copy-paste detour or a slow SSH session. The node row stays on `tunnel (waiting)` indefinitely; it does not time out or show an error on its own. If more than 15 minutes have passed, open **Edit node** on that row, click **Regenerate enrollment token**, and repeat the save-and-run-compose steps with the fresh file. The previous tunnel attempt is closed automatically, so there is nothing to clean up on the remote host first.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/enroll-a-remote-node/edit-node-regenerate.png" alt="Edit node dialog for the media-server Pilot Agent node, showing a card labeled 'Re-enroll the agent if the container was lost or the enrollment token expired' with a Regenerate enrollment token button." />
|
||||
</Frame>
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Multi-node" icon="sitemap" href="/features/multi-node">
|
||||
Distributed API Proxy mode, Sencho Mesh, license inheritance across nodes, and full troubleshooting.
|
||||
</Card>
|
||||
<Card title="Pilot Agent" icon="satellite-dish" href="/features/pilot-agent">
|
||||
Credential lifecycle, resource limits, and pilot-specific troubleshooting for the tunnel itself.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
title: Catch and Fix a Container That's Drifted From Its Compose File
|
||||
sidebarTitle: Catch and fix drift
|
||||
description: Read a real Drift finding after a manual out-of-band container change, then redeploy through Sencho to bring the running state back in line with the compose file.
|
||||
---
|
||||
|
||||
Say an on-call engineer hotfixes a container directly on the host during an incident: change the image tag, run `docker compose up -d`, done, the site's back up. Nobody thinks to also make the same edit through Sencho, so the compose file it deploys from tomorrow never learns about tonight's fix. This walks through catching exactly that: read a real Drift finding after an out-of-band container change, confirm what's different, and redeploy through Sencho to bring the running state back in line with the compose file.
|
||||
|
||||
This tutorial covers the runtime drift detection described on the Drift feature page: the status badge, the findings list, the drift ledger, and resolving a finding by redeploying. It does not cover Blueprint's separate policy-based drift mode (observe/suggest/enforce), which reconciles drift on Blueprint-deployed stacks automatically instead of leaving it for you to redeploy; see [Blueprint Model](/features/blueprint-model). It also does not cover network-attachment drift findings, which reuse this same mechanism but compare against the Networking tab instead of an image; see [Compose Networking](/features/compose-networking).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Any role that can view a stack, including the read-only **viewer** and **auditor** roles, can open the Drift tab and click **re-check**. Actually resolving drift means redeploying, which needs the `stack:deploy` permission for this stack (the same permission Start, Stop, Restart, and Update already require).
|
||||
- The stack must have been deployed through Sencho at least once. Drift's "since last deploy" signal has nothing to compare against until then, and shows **No deploy baseline** instead of a useful state.
|
||||
- No plan requirement and nothing to turn on. Drift detection runs automatically as soon as a stack has a compose file and Docker is reachable on the node.
|
||||
- Shell or SSH access to the host, if you want to reproduce this tutorial's own out-of-band change yourself. Any real manual Docker operation that changes a running container without going back through Sencho works just as well.
|
||||
|
||||
This tutorial uses a small single-service stack called `landing-page`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
image: nginx:1.27-alpine
|
||||
restart: always
|
||||
ports:
|
||||
- "8096:80"
|
||||
```
|
||||
|
||||
Deploy this stack (or adapt an existing one) before continuing.
|
||||
|
||||
<Steps>
|
||||
<Step title="Open the Drift tab and confirm the baseline">
|
||||
Click the stack in the sidebar to open it, then switch to the **Drift** tab in the right-hand panel (scroll the tab row if it doesn't fit). Right after a deploy through Sencho, the status reads **in sync** ("Runtime matches the compose file.") and the card below it reads **matches last deploy** ("The compose source is unchanged since the last deploy."). This is the state the rest of this tutorial gets you back to.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/resolve-stack-drift/drift-baseline-in-sync.png" alt="The Drift tab showing In sync status (Runtime matches the compose file) and Matches last deploy below it, with no findings or drift history." />
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Make an out-of-band change on the host">
|
||||
Simulate the incident hotfix: on the host, point the compose file at a different image tag, bring it up directly with Compose, then revert the file without redeploying through Sencho.
|
||||
|
||||
```bash
|
||||
cd /path/to/landing-page
|
||||
sed -i 's/nginx:1.27-alpine/nginx:1.29-alpine/' compose.yaml
|
||||
docker compose up -d
|
||||
sed -i 's/nginx:1.29-alpine/nginx:1.27-alpine/' compose.yaml
|
||||
```
|
||||
|
||||
The running container is now on `nginx:1.29-alpine`, but the compose file on disk is back to declaring `nginx:1.27-alpine`, exactly as if someone had made a quick fix and never told Sencho about it.
|
||||
</Step>
|
||||
|
||||
<Step title="Read the drift finding">
|
||||
Reopen the Drift tab. Just opening it re-runs the comparison, so the status flips to **drifted · 1 finding** ("Runtime differs from the compose file."), while **matches last deploy** stays exactly as it was: the file itself never changed, only what's running under it. Under **Findings**, the `app` service shows an **image** finding with the expected and actual images side by side.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/resolve-stack-drift/drift-detected-image-finding.png" alt="The Drift tab showing Drifted status with 1 finding, Matches last deploy still green below it, and a Findings section with an app image finding: compose nginx:1.27-alpine to running nginx:1.29-alpine." />
|
||||
</Frame>
|
||||
|
||||
This is the moment the two signals' independence actually matters: a stack can be drifted at runtime while its file is unchanged, or the reverse. Here it's drifted with an unchanged file, because the fix happened directly against Docker, not through a compose edit.
|
||||
</Step>
|
||||
|
||||
<Step title="Log the finding with re-check">
|
||||
Click **re-check**. A **Drift history** section appears with the same finding marked **OPEN** and a detected timestamp, and the header now reads "checked just now." The status badge above it, though, still says **drifted**.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/resolve-stack-drift/drift-recheck-still-drifted.png" alt="The Drift tab still showing Drifted status after clicking re-check, with a new Drift history section showing an app image finding marked OPEN, detected just now." />
|
||||
</Frame>
|
||||
|
||||
Re-check is a read plus a ledger write: it records the finding into the stack's history and logs a **Drift detected** entry to the Activity tab, but it never touches the running container. Nothing about the actual deployment has changed yet.
|
||||
</Step>
|
||||
|
||||
<Step title="Resolve it: redeploy through Sencho">
|
||||
Click **Update**. The **Update Readiness** dialog opens with a **Drift** warning already listed: "1 open drift finding: the running state has diverged from the compose file, so the rollback target may not match what is running." Here that's expected, since you already know why; on a stack you didn't just break yourself, that warning is exactly the kind of thing worth reading before you proceed. Click **Update now**.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/resolve-stack-drift/update-readiness-drift-warning.png" alt="The Ready to update dialog for landing-page, showing Ready with warnings and a Drift warning: 1 open drift finding, the running state has diverged from the compose file, so the rollback target may not match what is running." />
|
||||
</Frame>
|
||||
|
||||
Sencho re-pulls and recreates the `app` container from the compose file, which puts it back on `nginx:1.27-alpine`.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verify it worked
|
||||
|
||||
Check from two independent surfaces.
|
||||
|
||||
Back on the **Drift** tab, the status returns to **in sync**, and the drift history entry that was **OPEN** a moment ago now reads **RESOLVED**, with both a detected and a resolved timestamp:
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/resolve-stack-drift/drift-resolved-in-sync.png" alt="The Drift tab back to In sync status, with the drift history entry now marked RESOLVED, showing both a detected and a resolved timestamp." />
|
||||
</Frame>
|
||||
|
||||
On the **Activity** tab, which lists most-recent-first, the update entries sit above a new **Drift resolved** entry, which sits above the original **Drift detected** entry from earlier in this tutorial: read bottom to top, that's detected, then updated, then resolved, in the order the events actually happened.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/resolve-stack-drift/activity-drift-resolved.png" alt="The Activity tab showing, in order: landing-page updated, landing-page update started, Drift resolved on landing-page: 1 finding cleared, and Drift detected on landing-page: 1 new finding." />
|
||||
</Frame>
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
**Clicking re-check doesn't fix anything, and the status still says drifted.** That's not a bug, it's the read-only design: re-check only reconciles the ledger (recording what it currently sees, as in the screenshot above) so the finding shows up in history and on the Activity timeline. It never changes what Docker is running. If you want the drifted container gone, redeploy the stack instead (**Update**, or **Save & Deploy** from the compose editor) as in the last step above; that's the only action that actually reconciles the runtime with the compose file.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Stack Drift" icon="magnifying-glass-chart" href="/features/stack-drift">
|
||||
Full mechanics: every finding type, image comparison rules, port ranges, the drift ledger, and troubleshooting.
|
||||
</Card>
|
||||
<Card title="Stack Activity" icon="clock-rotate-left" href="/features/stack-activity">
|
||||
Drift detected and resolved events sit in the same timeline as deploys and restarts.
|
||||
</Card>
|
||||
<Card title="Blueprint Model" icon="drafting-compass" href="/features/blueprint-model">
|
||||
Blueprints manage drift differently, with a separate observe/suggest/enforce policy mode.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
title: Schedule an Operation
|
||||
sidebarTitle: Schedule an operation
|
||||
description: Create a recurring stack restart in Sencho, verify it with a manual run, and read its execution history.
|
||||
---
|
||||
|
||||
Say you run a small web app that slowly accumulates stale connections as the week goes on, and the fix is a restart every night while nobody is using it. You could log in at 3:00 AM, or you could let Sencho do it for you. This walks through creating a scheduled task that restarts the `my-website` stack (an nginx web tier with a postgres database) every day at 3:00 AM, running it once by hand to confirm it works, and checking the execution history.
|
||||
|
||||
By the end, the task is running on its own schedule, and you have a green run in the history that proves it works.
|
||||
|
||||
This tutorial covers one recurring **Restart Stack** task on the hub. It does not cover the other scheduled actions (auto-update, vulnerability scans, prunes, fleet snapshots), advanced cron expressions, per-service restart scoping, one-shot tasks, or scheduling actions against a remote node. See the [Scheduled Operations](/features/scheduled-operations) feature page for the full action list and the [cron expression reference](/features/scheduled-operations#cron-expression-reference).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An **admin** account on the Sencho hub. Creating and managing schedules requires admin.
|
||||
- A running stack on the hub. This tutorial schedules a restart of `my-website`, deployed as an nginx service with a postgres service behind it.
|
||||
- The hub (Local) selected as the active node. Schedules is a hub-level view: it is not available while a remote node is the active selection. If you manage remote nodes, switch back to **Local** before continuing.
|
||||
|
||||
<Note>
|
||||
A scheduled restart restarts the stack's containers in place. It does not recreate them, so the compose configuration and any bind-mounted data are left untouched.
|
||||
</Note>
|
||||
|
||||
<Steps>
|
||||
<Step title="Open the Schedules page">
|
||||
In the top navigation, click **More**, then under **Operations** pick **Schedules**.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/schedule-an-operation/more-menu-schedules.png" alt="The More navigation menu with Audit under Security & review, Logs, Update, and Schedules under Operations, and Console under Tools." />
|
||||
</Frame>
|
||||
|
||||
The page opens on the **Timeline** view: a 24-hour strip with five lanes, one per operation category (Lifecycle, Updates, Security, Upkeep, Backups). With no tasks yet, it shows the empty-state message `Nothing scheduled in the next 24 hours`.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/schedule-an-operation/schedules-timeline-empty.png" alt="The Scheduled Operations page on the Timeline view, showing the Next 24 hours header, an empty Next readout, the five category lanes, and the message 'Nothing scheduled in the next 24 hours. Toggle to All tasks to see every schedule, or create a new one.'" />
|
||||
</Frame>
|
||||
</Step>
|
||||
<Step title="Open the New Schedule form">
|
||||
Click **New Schedule** in the top right of the page. The **New scheduled task** modal opens.
|
||||
|
||||
Open the **Action** picker. The actions are grouped by category, each with a risk badge and one-line helper text so you can see the blast radius before you commit: **Lifecycle** holds the stack and container actions, **Updates** the auto-update variants, **Security** the vulnerability scan, **Upkeep** the resource prune, and **Backups** the fleet snapshot.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/schedule-an-operation/new-schedule-action-picker.png" alt="The New scheduled task modal with the Action picker open, showing the Lifecycle, Updates, Security, Upkeep, and Backups groups with their actions listed." />
|
||||
</Frame>
|
||||
</Step>
|
||||
<Step title="Configure the nightly restart">
|
||||
Fill in the form:
|
||||
|
||||
- **Name**: `Nightly website restart`
|
||||
- **Action**: **Restart Stack** (the default, in the Lifecycle group). The **Interruptive** badge and the helper text "Restarts containers in place" describe exactly what a run will do: stop and start the containers on the same configuration.
|
||||
- **Node**: **Local**.
|
||||
- **Stack**: `my-website`. The stack defines two services, so a **Services (leave empty for all)** grid appears with `web` and `db` checkboxes. Leave both unchecked to restart the entire stack.
|
||||
- **Schedule**: leave **Simple** mode selected, with **Daily**, **Hour** `03`, and **Minute** `00`. The preview below the controls reads `At 03:00 AM · 0 3 * * *`, and the note under it says the schedule runs in the node's local timezone. This is where you pick the off-peak window: 3:00 AM, when nobody is hitting the site.
|
||||
- **Enabled**: leave ON.
|
||||
- **Delete after successful run**: leave unchecked. This task is meant to fire every night, not remove itself after one success.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/schedule-an-operation/create-task-filled.png" alt="The New scheduled task modal configured with Name 'Nightly website restart', Action Restart Stack with the Interruptive badge, Node Local, Stack my-website with the web and db service checkboxes, a Daily schedule at 03:00 with the preview 'At 03:00 AM · 0 3 * * *', and the Enabled switch on." />
|
||||
</Frame>
|
||||
|
||||
Click **Create**. The modal closes and a toast confirms the task was created.
|
||||
</Step>
|
||||
<Step title="Confirm the task in the timeline and the table">
|
||||
The timeline now shows a blue pill on the **Lifecycle** lane at the next firing, and the **Next** readout shows the task name with a countdown.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/schedule-an-operation/timeline-pill.png" alt="The Scheduled Operations timeline with a blue pill labelled 23:00 my-website on the Lifecycle lane, and the Next readout showing 'Nightly website restart · in 20h 21m'." />
|
||||
</Frame>
|
||||
|
||||
Note the time: the schedule fires at 3:00 AM **in the node's timezone**, and the timeline displays firing times **in your browser's timezone**, so the pill time may differ from the form's preview. Here the node is on UTC, the browser on Eastern Daylight Time, and 03:00 UTC shows as 23:00.
|
||||
|
||||
Toggle to **All tasks** for the tabular view. The new row shows the name, the **Restart Stack** action badge, the `my-website` target, the human-readable schedule over its raw cron `0 3 * * *`, **Status** `Never run`, the next firing in your local time, and the **Enabled** switch set to ON.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/schedule-an-operation/all-tasks-row.png" alt="The All tasks table with one row: Nightly website restart, Restart Stack, my-website, At 03:00 AM / 0 3 * * *, Status Never run, Next Run 8/6/2026 11:00:00 PM, Enabled ON, and the Run now, Execution history, Edit, and Delete buttons." />
|
||||
</Frame>
|
||||
</Step>
|
||||
<Step title="Run the task now to prove it works">
|
||||
Click the **Run now** button (play icon) on the row. The task executes immediately instead of waiting for the next firing. After a few seconds, click **Refresh**; the row's **Status** flips to **Success** (green badge).
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/schedule-an-operation/all-tasks-success.png" alt="The All tasks table with the Nightly website restart row now showing a green Success badge in the Status column." />
|
||||
</Frame>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verify it worked
|
||||
|
||||
Check from two places, since either alone could be misleading (a table that has not refreshed, or a run that updated the badge but left an error behind).
|
||||
|
||||
**The Status badge.** In **All tasks**, the row shows the green **Success** badge, and the task is still **Enabled**, so the next firing is still scheduled.
|
||||
|
||||
**The Execution history.** Click the **Execution history** button (clock icon) on the row. The sheet opens with the breadcrumb `Schedules › Nightly website restart › Runs`, a `1 run` count, and the run's row: **Source** `Manual` (it was triggered by Run now, not by the cron), **Status** `Success`, a short **Duration**, and the details `Restarted 2 container(s) in stack "my-website"`: both services, exactly as configured.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/schedule-an-operation/run-history.png" alt="The Execution history sheet for Nightly website restart, showing 1 run with Source Manual, Status Success, Duration 0.7s, and Details 'Restarted 2 container(s) in stack my-website'. The footer shows the next scheduled run." />
|
||||
</Frame>
|
||||
|
||||
The footer confirms the schedule is alive: the next run time is shown there too, matching the row's **Next Run** in the table. You can now rely on the nightly restart without being at the console at 3:00 AM.
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
**You don't see Schedules in the More menu.** Schedules is a hub-level view. If a remote node is the active selection, the More menu shows only **Console**, and the hub-only views (Schedules, Audit, Logs, Update) are missing. Click the node switcher next to the Sencho logo, pick **Local**, and reopen **More**: Schedules is back under **Operations**.
|
||||
|
||||
A run that fails shows a red **Failed** badge in the row and an error-level notification; the failure notification carries the task name and the error so you can diagnose without opening the run history. The task stays enabled and fires again at its next cron tick, or you can click **Run now** to retry immediately. See [Scheduled Operations · Troubleshooting](/features/scheduled-operations#troubleshooting) for the full failure list.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Scheduled Operations" icon="clock" href="/features/scheduled-operations">
|
||||
Every supported action, the cron expression reference, and failure notifications.
|
||||
</Card>
|
||||
<Card title="Auto-Update Policies" icon="arrows-rotate" href="/features/auto-update-policies">
|
||||
Schedule image updates the same way, then review what changed on the companion board.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: Deploy a Stack Automatically From Your CI Pipeline
|
||||
sidebarTitle: Deploy from CI on push
|
||||
description: Create a signed webhook that lets an external pipeline trigger a real stack redeploy, and verify it end to end with a signed request standing in for your CI job.
|
||||
---
|
||||
|
||||
Say a small nginx-backed service, `storefront`, gets rebuilt every time its image changes. Without a webhook, redeploying it after a new build means someone opening Sencho and clicking **Update** by hand. This walks through wiring a webhook so an external pipeline can trigger that redeploy itself: you'll create the webhook, stage a new image tag the way a pipeline would, fire a signed HTTP request that stands in for the pipeline's own call, and confirm the container actually recreated on the new image.
|
||||
|
||||
This tutorial covers creating and triggering one webhook for the **Deploy (down + up)** action. It doesn't cover the other five actions (Restart, Stop, Start, Pull & Update, Git source sync), writing CI YAML for a specific provider, or key rotation across many webhooks; see the [Webhooks](/features/webhooks) feature page for the complete picture.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The `admin` role specifically. Creating, editing, and deleting webhooks needs the `system:webhooks` permission, which only `admin` holds; `node-admin`, `deployer`, `viewer`, and `auditor` can view a read-only list but not manage one.
|
||||
- A running stack to target. If you don't have one, select **Create Stack**, name it `storefront`, and replace its Compose file with:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.27-alpine
|
||||
restart: always
|
||||
ports:
|
||||
- "8095:80"
|
||||
```
|
||||
|
||||
Select **Save & Deploy**. This tutorial's steps and screenshots use this exact stack and tag, so if you use a different one, adjust the specific image tag named in step 3 to match.
|
||||
- The **Local** node active in the node switcher. The Webhooks page only appears while Local is active, and a webhook's execution is pinned to whichever node was active when you created it.
|
||||
- A way to send a signed HTTP POST request. This tutorial uses `curl` and `openssl` from a terminal to stand in for your CI platform's HTTP step.
|
||||
- Available on every tier; no Admiral requirement.
|
||||
|
||||
<Steps>
|
||||
<Step title="Create the webhook">
|
||||
Open **Settings → Automation → Webhooks** and select **Create webhook**. Fill in **Name** (`Deploy storefront`), pick **Stack** (`storefront`), and leave **Action** on its default, **Deploy (down + up)**. **Node** is read-only and shows **Local**: execution stays pinned to whichever node was active when you created the webhook, regardless of which node is active later.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-a-webhook/webhook-create-form-filled.png" alt="New webhook form with Name set to Deploy storefront, Stack set to storefront, Node showing Local, and Action set to Deploy (down + up)." />
|
||||
</Frame>
|
||||
|
||||
Select **Create**.
|
||||
</Step>
|
||||
<Step title="Copy the secret before you dismiss it">
|
||||
A green callout appears once, showing the full signing secret in a monospace block. Copy it now: every later view of this webhook, in the list or the API, shows only the masked form (`********` plus the last four characters). If you dismiss the callout without copying it, the only fix is to delete the webhook and create a new one, which rotates the secret and the trigger URL's id.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-a-webhook/webhook-secret-reveal.png" alt="Green success callout reading Webhook created. Copy your secret now, with the full secret in a monospace block above a Copy button. Below, the configured-webhooks list shows the Deploy storefront card with its trigger URL and masked secret." />
|
||||
</Frame>
|
||||
|
||||
Note the numeric id in the **Trigger URL** (`/api/webhooks/<id>/trigger`); you'll need both the id and the secret in the next steps.
|
||||
</Step>
|
||||
<Step title="Stage a change for the webhook to apply">
|
||||
A trigger against a stack that already matches its compose file is a no-op: Sencho still reports success, but nothing visibly changes, the same way a plain `docker compose up -d` only recreates a container whose configuration actually changed. To see a real redeploy, stage a change first, the way your pipeline would after a new build.
|
||||
|
||||
Open the `storefront` stack, edit `compose.yaml`, and bump the pinned tag from `nginx:1.27-alpine` to `nginx:1.28-alpine`. Instead of **Save & Deploy**, open the dropdown next to it and select **Save Only**. This writes the change to disk without deploying it, the same gap that exists in a real pipeline between committing a new image reference and the moment something actually applies it.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-a-webhook/webhook-save-only-menu.png" alt="Compose editor for storefront with the image line changed to nginx:1.28-alpine, and the Save & Deploy dropdown open showing Save Only and Discard Changes. The running container's logs below still show nginx/1.27.5, confirming the change is staged but not yet deployed." />
|
||||
</Frame>
|
||||
</Step>
|
||||
<Step title="Trigger it like your pipeline would">
|
||||
Compute an HMAC-SHA256 signature over the exact request body using the secret from step 2, and send it in the `X-Webhook-Signature` header with a `sha256=` prefix:
|
||||
|
||||
```bash
|
||||
SECRET="your-webhook-secret"
|
||||
BODY='{}'
|
||||
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
|
||||
|
||||
curl -X POST https://your-sencho.example.com/api/webhooks/<id>/trigger \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Webhook-Signature: sha256=$SIGNATURE" \
|
||||
-d "$BODY"
|
||||
```
|
||||
|
||||
A valid request returns `202 Accepted` with `{"message":"Webhook accepted","action":"deploy"}` immediately. That confirms the signature checked out and the deploy is now running asynchronously, not that it has finished.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verify it worked
|
||||
|
||||
Check from two independent surfaces so you're not trusting a single UI element.
|
||||
|
||||
**The stack itself.** Reopen `storefront`. The container recreated on the new image: a fresh uptime, and its logs now start with `nginx/1.28.3` instead of `1.27.5`.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-a-webhook/webhook-storefront-redeployed.png" alt="storefront stack page showing storefront-web-1 up 22 seconds, with logs starting from nginx/1.28.3, and the compose editor showing image nginx:1.28-alpine." />
|
||||
</Frame>
|
||||
|
||||
**The webhook's own history.** Back on **Settings → Automation → Webhooks**, expand **Recent executions** on the card. A green check confirms `deploy` succeeded, with the timestamp and duration of the run you just triggered.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-a-webhook/webhook-recent-executions.png" alt="Deploy storefront webhook card with Recent executions expanded, showing one entry: a green check, deploy, a timestamp, and a duration of 4.1 seconds." />
|
||||
</Frame>
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
**The trigger returns `404 Webhook not found or signature invalid`, but the id and secret are both right.** This is almost always a signing mismatch, and the most common cause is a shell adding bytes you didn't sign. `echo "$BODY"` appends a trailing newline that `echo -n "$BODY"` does not, so if any step in your pipeline signs with one and sends with the other, the signature won't match the bytes Sencho actually received, and it returns the same generic 404 it returns for every unauthenticated case (unknown id, disabled webhook, missing header). Confirm you're signing the exact same string you send, byte for byte, with no incidental newline.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Webhooks" icon="satellite-dish" href="/features/webhooks">
|
||||
The full action list, concurrency behavior, execution history retention, and security model.
|
||||
</Card>
|
||||
<Card title="Git Sources" icon="code-branch" href="/features/git-sources">
|
||||
The Git source sync action pulls the latest commit and deploys, an alternative to bumping a pinned tag by hand.
|
||||
</Card>
|
||||
<Card title="Auto-Update Policies" icon="arrows-rotate" href="/features/auto-update-policies">
|
||||
A hands-off way to keep a stack current on a schedule, without needing an external pipeline to call a webhook at all.
|
||||
</Card>
|
||||
<Card title="Deploy Compose Changes by Pulling From Git Instead of Editing by Hand" icon="code-branch" href="/tutorials/connect-a-git-source">
|
||||
Link a stack to a Git repository and run the same pull-review-apply cycle by hand from the panel, no webhook required.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Stop a Vulnerable Deploy Before It Starts
|
||||
sidebarTitle: Block a vulnerable deploy
|
||||
description: Add a scan policy that rejects a deploy carrying Critical vulnerabilities, read the block dialog, and override it as an admin when the risk is acceptable.
|
||||
---
|
||||
|
||||
Say a stack's base image quietly falls behind between one deploy and the next and picks up a Critical CVE. Nothing about a normal `docker compose up` stops that image from going live. This walks through adding a scan policy that gates deploys against a small `prod-web` stack running `nginx:1.14`, an image old enough to reliably carry dozens of Critical and High findings: Trivy scans every image the stack references before any container starts, and Sencho rejects the deploy instead of starting one with a known-bad image. You'll then decide, as an admin, whether to accept the risk for this one deploy and confirm that decision lands in the audit log.
|
||||
|
||||
This tutorial covers one block-on-deploy policy scoped to a single stack pattern, its block dialog, and the admin bypass. It doesn't cover installing the Trivy scanner itself, CVE suppressions, or replicating a policy across a fleet through Fleet Federation; see the [Deploy Enforcement](/features/deploy-enforcement) feature page for the complete picture.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The `admin` role. Creating and editing a policy needs the `stack:edit` permission, which both the `admin` and `node-admin` roles hold, but only `admin` can click **Deploy anyway** on a blocked deploy: `node-admin` can configure policies but not bypass a block.
|
||||
- The Trivy scanner installed and running on the node you're deploying to (**Security** → **Scanner setup**). Deploy Enforcement fails open, letting the deploy through with a warning alert instead of a block, when Trivy isn't installed.
|
||||
- Available on every tier; no Admiral requirement.
|
||||
- This tutorial uses a small stack called `prod-web`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.14
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8091:80"
|
||||
```
|
||||
|
||||
`nginx:1.14` is used here specifically because it's old enough to reliably carry Critical and High findings, which is what makes the block in this tutorial real instead of staged. Create this stack (or use one you already have with a similarly outdated image), but don't start it yet: the point of this tutorial is watching the policy catch the first deploy attempt.
|
||||
|
||||
<Steps>
|
||||
<Step title="Add a block policy scoped to your stack">
|
||||
Open **Security** → **Policies** and select **Add policy**. Set **Name** to `Production block on critical` and **Stack pattern (optional)** to `prod-*`, so the policy only ever evaluates stacks whose name starts with `prod-`. Leave **Known-exploited (KEV)** and **Fixable Critical/High** on (both default on), turn on **Severity threshold** (it defaults to **Critical**), and turn on **Block on deploy**. Leave **Enabled** on.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-deploy-enforcement/new-policy-form-filled.png" alt="New policy form filled in: Name Production block on critical, Stack pattern prod-*, Severity threshold ON at Critical, Known-exploited (KEV) ON, Fixable Critical/High ON, Block on deploy ON, Enabled ON." />
|
||||
</Frame>
|
||||
|
||||
At least one block condition has to stay on; a policy with **Block on deploy** enabled but every condition off is rejected as a no-op.
|
||||
</Step>
|
||||
<Step title="Confirm the policy is live">
|
||||
Select **Create**. The policy now appears as a card reading **max: CRITICAL**, **KEV**, **Fixable**, and a destructive **block** badge, with **Scope: prod-\*** underneath.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-deploy-enforcement/policy-created-prod-block.png" alt="Policies tab showing the new Production block on critical policy card with max: CRITICAL, KEV, Fixable, and block badges, and Scope: prod-*." />
|
||||
</Frame>
|
||||
</Step>
|
||||
<Step title="Deploy the stack and watch the gate fire">
|
||||
Open the `prod-web` stack and select **Start**. Sencho enumerates the stack's images, runs a pre-flight Trivy scan against `nginx:1.14`, and evaluates the result against the policy before any container starts. This is the first scan of this image digest, so budget up to a couple of minutes rather than the sub-second result a cached rescan gets.
|
||||
|
||||
Instead of a running container, a **Deploy blocked by security policy** dialog appears, naming the policy and every condition it blocks on. The violation row shows `nginx:1.14` with its finding counts and a **Severity**, **KEV**, and **Fixable** badge, each matching a condition the image tripped.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-deploy-enforcement/deploy-blocked-dialog.png" alt="Deploy blocked by security policy dialog for prod-web, naming policy Production block on critical, with a violation row for nginx:1.14 showing 31 CRITICAL, 82 HIGH, 1 KEV, 77 FIXABLE, Severity/KEV/Fixable badges, a CRITICAL chip, and Close and Deploy anyway buttons." />
|
||||
</Frame>
|
||||
|
||||
This is a hard gate, not an advisory: the stack has no running containers and stays that way until the image clears the policy or an admin overrides it.
|
||||
</Step>
|
||||
<Step title="Bypass the block as admin">
|
||||
Select **Deploy anyway**. The button is only active because you're signed in as `admin`; a non-admin session sees a disabled **Admin required to bypass** button in its place and cannot proceed. Sencho retries the deploy with the block ignored, pulls the image, starts the container, and runs its usual health-gate observation window.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-deploy-enforcement/stack-running-after-bypass.png" alt="prod-web stack page showing RUNNING status with prod-web-web-1 up and port 8091 mapped to 80/tcp." />
|
||||
</Frame>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verify it worked
|
||||
|
||||
Check from two independent surfaces so you're not trusting a single UI element.
|
||||
|
||||
**The stack itself.** `prod-web` shows **RUNNING**, with `prod-web-web-1` up and its port mapping live, as in the screenshot above.
|
||||
|
||||
**The audit log.** Open **More** → **Audit**. Reading newest first, you'll see the successful bypass deploy, a `policy.bypass` entry naming the policy, the violation count, and the offending image (`policy.bypass stack="prod-web" policy="Production block on critical" violations=1 images=[nginx:1.14]`), and below that the original blocked attempt with a `409` status in red. All three carry the account that triggered them, so the override is attributable, not anonymous.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-deploy-enforcement/audit-log-bypass-entry.png" alt="Audit log showing three prod-web entries in order: a 200 deploy, a policy.bypass entry naming policy Production block on critical with violations=1 images=[nginx:1.14], and a 409 blocked deploy." />
|
||||
</Frame>
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
**The gate lets a deploy through even though a policy should have caught it.** The most common first-time cause is a stack pattern that doesn't actually match the stack name. Edit the policy and set **Stack pattern** to something that doesn't match, like `staging-*`, while your stack is still named `prod-web`:
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-deploy-enforcement/policy-pattern-mismatch.png" alt="Policies tab showing the Production block on critical policy with Scope: staging-*, which does not match the prod-web stack." />
|
||||
</Frame>
|
||||
|
||||
Deploy or update `prod-web` again and it succeeds with no block at all: the policy never evaluates a stack whose name doesn't match its pattern, so a typo here fails silently rather than loudly. Fix the pattern back to `prod-*` (or clear it entirely to apply the policy to every stack on the node) and redeploy to confirm the gate fires again. See [Deploy Enforcement · Troubleshooting](/features/deploy-enforcement#troubleshooting) for the other ways a policy can fail to fire, including a missing Trivy install and a scan that found nothing matching your conditions.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Deploy Enforcement" icon="shield-check" href="/features/deploy-enforcement">
|
||||
The full mechanics: precedence between policies, every code path the gate covers, and fleet policy replication.
|
||||
</Card>
|
||||
<Card title="Vulnerability Scanning" icon="shield-halved" href="/features/vulnerability-scanning">
|
||||
Install and configure the Trivy scanner this tutorial's policy depends on.
|
||||
</Card>
|
||||
<Card title="CVE Suppressions" icon="eye-slash" href="/features/cve-suppressions">
|
||||
Accept a specific finding with an expiry so it stops counting toward a block, without disabling the whole policy.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Move a Blueprint Deployment to a New Node
|
||||
sidebarTitle: Move a blueprint to a new node
|
||||
description: Relocate a Blueprint-managed stack from a node you're retiring to another node in the fleet, without letting the reconciler undo the move partway through.
|
||||
---
|
||||
|
||||
Say a small stateless service, a status page, a metrics exporter, an internal API, runs as a [Blueprint](/features/blueprint-model)-managed deployment on one node in your fleet. That node is going away, for maintenance, decommissioning, or a hardware swap, and you need the workload running on a different node before you take the old one offline. This walks through doing that with **Federation**'s pin and cordon controls: pin the blueprint to the destination so the reconciler stops trusting its normal selector, cordon the source so it stops picking up anything new, then confirm the rollout that actually moves the workload.
|
||||
|
||||
<Note>
|
||||
Despite the name, Federation doesn't connect separate Sencho instances or fleets together, that's a different concept Sencho doesn't do at all. Federation is a tab inside a single fleet's **Fleet** view. It steers *where* that one fleet's Blueprint-managed deployments land; it has no notion of "other fleets."
|
||||
</Note>
|
||||
|
||||
By the end, the workload runs only on the new node, the old node is marked unschedulable, and you can confirm both from more than one place in the UI.
|
||||
|
||||
Both controls follow the same rule, and it's the part most likely to trip up a first-time user: **pinning and cordoning only declare intent.** Neither one deploys or withdraws a single container by itself. The reconciler stages what it *wants* to do next; you still open the blueprint and click **Confirm Apply** before anything on the fleet actually changes. That extra step is what makes this walkthrough three actions long instead of one.
|
||||
|
||||
This tutorial does not cover creating a Blueprint from scratch (see [Create and Approve a Blueprint](/tutorials/create-and-approve-a-blueprint)), draining a node of everything it runs, or evicting a stateful deployment. See the [Blueprints](/features/blueprint-model) and [Fleet Federation](/features/fleet-federation) feature pages for those last two.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **An admin account.** Cordoning a node and pinning a blueprint both check the same permission, scoped to the node you're acting on; clearing a pin checks that same permission without a node scope. Delegating that permission to a non-admin role for one specific node is an Admiral-tier capability, so on Community you complete every step here as the instance's admin.
|
||||
- **An existing Blueprint already deployed on at least one node.** Federation steers placement for Blueprints that exist; it does not create them. If you don't have one yet, walk through [Create and Approve a Blueprint](/tutorials/create-and-approve-a-blueprint) first.
|
||||
- **At least two nodes in the fleet**, both online. Pinning only matters when there's somewhere else to pin to. Multi-node fleets and Federation are both Community-tier, no paid plan required.
|
||||
|
||||
<Frame caption="Federation before any changes: no nodes cordoned, one blueprint listed, unpinned.">
|
||||
<img src="/images/tutorials/set-up-fleet-federation/federation-tab-before-pin.png" alt="Fleet Federation tab showing Cordoned nodes 0 of 4 and a Pin policy table with one row for the edge-status blueprint, selector any=[dev], Pinned to (unpinned), Effective any=[dev]." />
|
||||
</Frame>
|
||||
|
||||
<Steps>
|
||||
<Step title="Pin the blueprint to the destination node">
|
||||
Open **Fleet → Federation** and find your blueprint's row in the **Pin policy** table. Open the **Pinned to** dropdown and select the node you're moving the workload to. A cordoned node would show inline as `<name> · cordoned` here, so you can see at a glance if you're about to pin onto something already out of rotation.
|
||||
|
||||
<Frame caption="The Pinned to dropdown, before selecting a destination.">
|
||||
<img src="/images/tutorials/set-up-fleet-federation/pin-dropdown-open.png" alt="Pinned to dropdown open for the edge-status blueprint, listing (unpinned) with a checkmark, Local, Pitt-Moba, SLX-Mars, and SLX-Orion." />
|
||||
</Frame>
|
||||
|
||||
The selection saves immediately and a toast confirms it. The **Effective** column updates to `pin: <destination>`, overriding whatever the selector would otherwise resolve to. Nothing has deployed yet, this only changes what the reconciler wants to do next.
|
||||
|
||||
<Frame caption="Pin saved. The Effective column now reads the pinned node.">
|
||||
<img src="/images/tutorials/set-up-fleet-federation/pin-applied-effective.png" alt="Pin policy table with the edge-status row now showing Pinned to SLX-Mars and Effective pin: SLX-Mars." />
|
||||
</Frame>
|
||||
</Step>
|
||||
<Step title="Cordon the node you're retiring">
|
||||
Go to **Fleet → Overview**. On the source node's card, click the kebab menu (`⋯`) and choose **Cordon node**.
|
||||
|
||||
<Frame caption="The node card's kebab menu, with Cordon node as the only Federation action available on an uncordoned node.">
|
||||
<img src="/images/tutorials/set-up-fleet-federation/node-actions-cordon-menu.png" alt="Node actions menu open on the Pitt-Moba card, listing Node details, Edit node, Delete node, Cordon node, and Mute." />
|
||||
</Frame>
|
||||
|
||||
A confirmation dialog opens with an optional reason field, up to 256 characters. Fill in something that will make sense to whoever looks at this node in a week.
|
||||
|
||||
<Frame caption="Cordon confirmation dialog with a reason filled in.">
|
||||
<img src="/images/tutorials/set-up-fleet-federation/cordon-confirm-dialog.png" alt="Cordon Pitt-Moba confirmation dialog. Description reads 'Mark this node as unschedulable. New blueprint deployments will skip it. Existing deployments remain in place.' Reason field filled with 'Retiring host, migrating workloads to SLX-Mars'." />
|
||||
</Frame>
|
||||
|
||||
Click **Cordon node**. The card gains a `Cordoned` pill immediately; this alone does not touch anything already running on the node, including the blueprint you're about to move off it.
|
||||
|
||||
<Frame caption="Node card carrying the Cordoned pill after confirming.">
|
||||
<img src="/images/tutorials/set-up-fleet-federation/node-card-cordoned.png" alt="Fleet Overview grid with the Pitt-Moba node card showing the amber Cordoned pill alongside its Online and remote badges. Local, SLX-Mars, and SLX-Orion have no cordon indicator." />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
Cordon only ever removes a node from consideration for *new* placements; it never touches what's already running, and it never overrides an explicit pin. If you pinned a blueprint to a node that happens to be cordoned, the pin still wins, the blueprint deploys there anyway once its rollout is confirmed. Cordon and pin answer different questions ("should this node pick up new work automatically" versus "where must this specific blueprint run"), and when they disagree, the explicit answer (pin) beats the general one (cordon).
|
||||
</Note>
|
||||
</Step>
|
||||
<Step title="Confirm the rollout that actually moves it">
|
||||
Pinning and cordoning only declared intent, neither one deployed or withdrew anything by itself. Open **Fleet → Deployments**, select your blueprint, and click **Apply now**. Sencho opens a rollout preview.
|
||||
|
||||
<Frame caption="Rollout preview for the migration: a safe create on the destination, a safe remove on the source.">
|
||||
<img src="/images/tutorials/set-up-fleet-federation/rollout-preview-migration.png" alt="Confirm rollout dialog for the edge-status blueprint reading Safe 2, Warnings 1, Blockers 0. Changes list: SLX-Mars (remote/online) create safe New placement, and Pitt-Moba (remote/online) remove safe Withdraw from selector." />
|
||||
</Frame>
|
||||
|
||||
You should see one **create** on the destination node and one **remove** on the source, both marked safe. If the destination were unreachable, that create would show as a blocker instead and **Confirm Apply** would stay disabled, see [If something goes wrong](#if-something-goes-wrong). Click **Confirm Apply**.
|
||||
|
||||
<Frame caption="After confirming: the deployment table now lists only the destination node, marked Pinned.">
|
||||
<img src="/images/tutorials/set-up-fleet-federation/blueprint-migrated-pinned-badge.png" alt="Blueprint detail sheet for edge-status showing a Pinned to SLX-Mars banner and a Deployments table with a single row: SLX-Mars, marked Pinned, status Active, last activity just now." />
|
||||
</Frame>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verify it worked
|
||||
|
||||
Check from more than one surface, since a single stale UI element could tell you the wrong thing.
|
||||
|
||||
**The Federation tab.** The Pin policy row's **Effective** column reads the pinned node, and the Cordoned nodes card lists the source node with its reason and a timestamp.
|
||||
|
||||
<Frame caption="Federation tab after the migration: source node cordoned, blueprint pinned to the destination.">
|
||||
<img src="/images/tutorials/set-up-fleet-federation/federation-tab-after-migration.png" alt="Fleet Federation tab showing Cordoned nodes 1 of 4 with Pitt-Moba listed and the reason 'Retiring host, migrating workloads to SLX-Mars', and a Pin policy row for edge-status with Pinned to SLX-Mars and Effective pin: SLX-Mars." />
|
||||
</Frame>
|
||||
|
||||
**The audit log.** Open **Audit** and search or scan for the actions you just took. Each mutating request gets its own row: the pin write, the cordon, and the apply.
|
||||
|
||||
<Frame caption="Audit log entries for the pin, cordon, and apply actions, in reverse chronological order.">
|
||||
<img src="/images/tutorials/set-up-fleet-federation/audit-log-federation-entries.png" alt="Audit log rows: admin post /api/blueprints/15/apply at 15:43:04, admin cordoned node: 7 at 15:40:48, and admin updated blueprint pin: 15 at 15:38:44." />
|
||||
</Frame>
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
The most common first-time confusion is pinning or cordoning and then expecting the workload to move on its own. It won't. Both actions only change what the reconciler *wants*; the blueprint's catalog tile and detail sheet footer show **pending** or **reapproval required** until you open the blueprint and confirm a rollout.
|
||||
|
||||
<Frame caption="A pinned blueprint sitting in pending state before the rollout is confirmed.">
|
||||
<img src="/images/tutorials/set-up-fleet-federation/blueprint-pinned-banner-pending.png" alt="Blueprint detail sheet showing a Pinned to SLX-Mars banner while the Deployments table still lists the original node, Pitt-Moba, as Active, and the sheet footer reads pending." />
|
||||
</Frame>
|
||||
|
||||
If **Confirm Apply** is greyed out instead, a blocker is present, most often an offline or unreachable destination node. Check the destination's status in **Fleet → Overview** before retrying, or change the pin to a node that's actually reachable.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Fleet Federation" icon="diagram-project" href="/features/fleet-federation">
|
||||
Cordon and pin mechanics, the confirm-before-mutate model, and full troubleshooting.
|
||||
</Card>
|
||||
<Card title="Blueprints" icon="sitemap" href="/features/blueprint-model">
|
||||
The declarative deployment model Federation steers, including selectors, drift policy, and the rollout-preview mechanic.
|
||||
</Card>
|
||||
<Card title="Create and Approve a Blueprint" icon="drafting-compass" href="/tutorials/create-and-approve-a-blueprint">
|
||||
Author and deploy a Blueprint from scratch, the prerequisite this tutorial assumes.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: Push a Shared Environment File to Every Node Running a Stack
|
||||
sidebarTitle: Push shared secrets to your fleet
|
||||
description: Author one encrypted env-var bundle on the control instance and push it to a stack running on every labeled node, with a diff you can read before anything is written.
|
||||
---
|
||||
|
||||
Say the same `inventory-api` stack runs on two nodes in your fleet, each with its own `.env` file, and you need to rotate `DB_PASSWORD` on both without SSHing into either host or risking one node drifting out of sync with the other. This walks through building an encrypted secret bundle on your control instance, targeting both nodes by a shared label, and pushing it with a preview step that shows exactly what will change before you write anything.
|
||||
|
||||
By the end, one bundle holds the source of truth for the stack's environment, and a single push keeps every labeled node's `.env` in sync with it.
|
||||
|
||||
This tutorial covers creating a bundle, targeting nodes by label, and reading the push preview and results. It doesn't cover importing an existing `.env` from a running stack, editing a bundle to a new version, or the full audit and encryption model; see the [Fleet Secrets](/features/fleet-secrets) feature page for those.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An **admin** account on the instance you're signed into. Fleet Secrets decrypts and writes credentials fleet-wide, so every route requires an administrator; there is no lesser role that can push a bundle.
|
||||
- You're working from the **control instance** (the hub), not a remote node. Fleet Secrets is a hub concern: its API routes never proxy to a remote node, so the **Secrets** tab and its data always reflect the instance you're currently signed into.
|
||||
- At least two nodes in your fleet (Local plus one enrolled node), each running a stack with the **same name** and declaring an env file via `env_file:` in its compose. This tutorial uses `inventory-api`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
api:
|
||||
image: nginx:alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8099:80"
|
||||
env_file:
|
||||
- .env
|
||||
```
|
||||
|
||||
Deploy this stack on both nodes before you start (an empty `.env` is fine; the push will populate it). A stack whose compose only has an inline `environment:` block won't show up as a push target, since Fleet Secrets writes to a file, not inline compose values.
|
||||
- Both nodes carry a **shared label**. This tutorial uses `inventory` on both. Add labels from **Settings → Nodes** if they aren't set yet.
|
||||
|
||||
<Note>
|
||||
Fleet Secrets is available on every Sencho installation; no Admiral requirement.
|
||||
</Note>
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Fleet and create a bundle">
|
||||
Open **Fleet**, select the **Secrets** tab, and click **New bundle** (or **Create your first bundle** if this is the first one on the instance).
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-fleet-secrets/secrets-tab-empty.png" alt="Fleet view with the Secrets tab selected, showing the empty state: heading 'One source of truth for env' and a Create your first bundle button." />
|
||||
</Frame>
|
||||
|
||||
Give the bundle a **Name** (`inventory-api-env` here) and an optional **Description**. Add a `KEY=value` row for each variable the stack needs; click **Add key** for more rows. This tutorial uses `LOG_LEVEL=info`, `REGION=us-east`, and `DB_PASSWORD` set to a placeholder value you'll rotate later. Add a **Change note** describing the initial save.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-fleet-secrets/create-bundle-filled.png" alt="New secret bundle sheet with Name inventory-api-env, Description 'Shared environment for the inventory-api stack', three key rows LOG_LEVEL=info, REGION=us-east, DB_PASSWORD=change-me-2026, and a change note reading 'Initial inventory-api bundle'." />
|
||||
</Frame>
|
||||
|
||||
Click **Save**. The values are encrypted before the ciphertext is written to disk; the plaintext only lives in the editor while you're filling it in.
|
||||
</Step>
|
||||
<Step title="Confirm the bundle was saved">
|
||||
The bundle now appears as a row in the **Secret bundles** table, at version `v1` with a key count matching what you entered.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-fleet-secrets/bundle-created.png" alt="Secret bundles table showing one row: inventory-api-env, description 'Shared environment for the inventory-api stack', version v1, 3 keys, with edit, send, and delete icons on the right." />
|
||||
</Frame>
|
||||
</Step>
|
||||
<Step title="Target the push by label and stack name">
|
||||
Click the **Send** icon on the bundle's row to open the push wizard. On the **Target** tab, toggle **any** or **all** (any is fine when you have one label) and pick the label both nodes carry, `inventory` here. Enter the **Stack name** the bundle should write to, exactly as it appears on each node. The **Env file** dropdown populates from `env_file:` entries a representative target's compose declares; leave it at `.env` unless your stack uses a different filename.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-fleet-secrets/push-target.png" alt="Push wizard Target tab. Target nodes section with the any toggle selected and the inventory label chosen. Target stack section with Stack name inventory-api and Env file dropdown showing .env." />
|
||||
</Frame>
|
||||
|
||||
Click **Preview**. This step only reads; it doesn't write anything yet.
|
||||
</Step>
|
||||
<Step title="Read the preview before writing anything">
|
||||
Each row is a node the label selector matched, with a summary of how many keys will be added, changed, or left unchanged. Expand a row to see the per-key breakdown. Since both nodes started with an empty `.env`, every key shows as added.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-fleet-secrets/push-preview.png" alt="Push wizard Preview tab with two expanded node rows, Local and warehouse-02, each showing +3 added, ~0 changed, ·0 unchanged, and per-key ADDED rows for DB_PASSWORD, LOG_LEVEL, and REGION." />
|
||||
</Frame>
|
||||
|
||||
This is the safety net: the diff shown here is exactly what the next click writes. If a row looks wrong, close the wizard, fix the bundle, and reopen Send to re-run Preview.
|
||||
</Step>
|
||||
<Step title="Push and confirm the result">
|
||||
Click **Push to N nodes**. Sencho writes to each matched node in sequence and reports a per-node outcome on the **Results** tab: a green check with the same added/changed/unchanged counts on success, or a red error with the exact failure string.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-fleet-secrets/push-results.png" alt="Push wizard Results tab with two green success rows: Local and warehouse-02, each showing +3 ~0 ·0." />
|
||||
</Frame>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verify it worked
|
||||
|
||||
Check from two independent surfaces so you're not trusting a single UI element.
|
||||
|
||||
**The Results tab itself**, shown above: both nodes report success with the counts matching the bundle's key count.
|
||||
|
||||
**The Audit Log.** Open **More → Audit**. Reading newest first, the top entry reads `pushed secret: 1` and the one just below it `previewed secret push: 1`, both attributed to the account that ran the push.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-fleet-secrets/audit-log-push.png" alt="Audit log showing two recent entries: 'admin pushed secret: 1' and 'admin previewed secret push: 1', each with a timestamp, node, and 200 status." />
|
||||
</Frame>
|
||||
|
||||
If you have shell access to either node, `cat`-ing the stack's `.env` file is a third way to confirm the write landed, but the two UI surfaces above are enough for day-to-day verification.
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
**A node lands in the failed column.** The most common first-time cause is the **Stack name** not matching exactly what's deployed on every target. Retarget the same bundle at a name with a typo, `inventory-apy` instead of `inventory-api`, and Preview still succeeds (it reads an empty, nonexistent file as "nothing set yet" and shows every key as added), but the actual push fails on both nodes: the local node returns an `ENOENT` filesystem error, and the proxied node returns `No env file exists for this stack` over HTTP.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-fleet-secrets/push-failed-typo.png" alt="Push wizard Results tab with two red failure rows: Local showing an ENOENT no such file or directory error, and warehouse-02 showing 'failed to write env (HTTP 404: No env file exists for this stack)'." />
|
||||
</Frame>
|
||||
|
||||
Preview cannot catch this class of mistake because it treats an unreadable target as an empty file rather than a missing stack. Fix the stack name on the Target tab and re-run Preview; a failed push is safe to retry immediately, since the overlay write is idempotent. See [Fleet Secrets · Troubleshooting](/features/fleet-secrets#troubleshooting) for the other common causes, including a stack whose compose doesn't declare the chosen env file at all.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Fleet Secrets" icon="key-round" href="/features/fleet-secrets">
|
||||
Versioning, the import-from-stack flow, concurrency and lifecycle behavior, and the full audit trail.
|
||||
</Card>
|
||||
<Card title="Enroll a Remote Node" icon="server" href="/tutorials/enroll-a-remote-node">
|
||||
Bring a second node into your fleet if you don't have one to label yet.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: Set Up SSO with Custom OIDC
|
||||
sidebarTitle: Set up SSO
|
||||
description: Connect Sencho to your identity provider so your team signs in with credentials they already have, verified with a real end-to-end login.
|
||||
---
|
||||
|
||||
Say your team already authenticates against a self-hosted identity provider, such as Keycloak, Authentik, or Authelia, and you'd rather they sign in to Sencho the same way than maintain a second password. This walks through registering Sencho as an OAuth client in that provider, wiring up Sencho's **Custom OIDC** provider to point at it, and confirming a real sign-in works end to end, using Keycloak as the worked example.
|
||||
|
||||
By the end, a **Sign in with your identity provider** button appears on the Sencho login page, and signing in through it auto-provisions a new Sencho account with the role you configured.
|
||||
|
||||
This tutorial covers **Custom OIDC** on Community, connected to a self-hosted provider. It does not cover the built-in **Google**, **GitHub**, or **Okta** presets (the same Sencho-side steps apply, with fewer fields to fill in), **LDAP / Active Directory** (Admiral only), **SSO only** mode, or mapping identity-provider groups to the Sencho Admin role. See the [SSO & LDAP Authentication](/features/sso) feature page for all of those, plus setup notes for other self-hosted providers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An **admin** account on the Sencho instance. Configuring SSO providers requires admin, on every tier.
|
||||
- An OIDC-compliant identity provider already running, with access to register a new OAuth client on it. This tutorial uses Keycloak; the Sencho-side steps are identical for Authentik, Authelia, or any other spec-compliant provider.
|
||||
- The identity provider must be reachable from Sencho over **HTTPS with a valid certificate**. A local dev instance behind plain HTTP, or a self-signed certificate Sencho doesn't trust, will not work.
|
||||
|
||||
<Warning>
|
||||
Sencho's OIDC discovery step rejects a plain `http://` Issuer URL outright, even for a same-host or `localhost` identity provider. If you're self-hosting your identity provider for the first time, put it behind a reverse proxy with a real certificate (Caddy with automatic HTTPS, or Traefik with Let's Encrypt) before starting this tutorial.
|
||||
</Warning>
|
||||
|
||||
<Steps>
|
||||
<Step title="Register Sencho as a client in your identity provider">
|
||||
In Keycloak, open **Clients → Create client**. Set **Client ID** to `sencho`, leave **Client type** as **OpenID Connect**, and click **Next**. On the **Capability config** step, turn **Client authentication** on (this makes it a confidential client with a secret, which the form in the next step expects) and click **Next**. On **Login settings**, set **Valid redirect URIs** to your Sencho URL plus the callback path Sencho's SSO page displays at the bottom of the provider list:
|
||||
|
||||
```
|
||||
https://<your-sencho-url>/api/auth/sso/oidc/oidc_custom/callback
|
||||
```
|
||||
|
||||
Click **Save**. Open the new client's **Credentials** tab and copy the **Client secret**; you'll paste it into Sencho in the next step.
|
||||
</Step>
|
||||
<Step title="Open Custom OIDC in Sencho and fill in the provider">
|
||||
In Sencho, click your avatar, choose **Settings**, then under **Access** pick **SSO**. Click the **Custom OIDC** card to expand it.
|
||||
|
||||
Fill in:
|
||||
|
||||
- **Display Name**: the label shown on the login button, for example `Corporate SSO`.
|
||||
- **Issuer URL**: your identity provider's realm URL, without the `/.well-known/openid-configuration` suffix. For Keycloak this is `https://<your-idp-url>/realms/<realm-name>`.
|
||||
- **Client ID**: `sencho`, matching what you set in step 1.
|
||||
- **Client Secret**: the secret you copied from the Credentials tab.
|
||||
- **Default Role**: leave as **Viewer**. This is the role a new SSO user gets unless they match the optional Admin Claim mapping (not covered here; see the feature page for group-based role mapping).
|
||||
|
||||
Leave **Admin Claim**, **Scopes**, and the claim-mapping fields blank; they fall back to sensible OIDC defaults for a standard Keycloak realm.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-sso/custom-oidc-filled.png" alt="The Custom OIDC card expanded and toggled ON, with Display Name 'Corporate SSO', an Issuer URL ending in /realms/master, Client ID 'sencho', a Client Secret entered, and Default Role set to Viewer." />
|
||||
</Frame>
|
||||
|
||||
Toggle the card's switch to **ON**, then click **Save**.
|
||||
</Step>
|
||||
<Step title="Test the connection">
|
||||
Click **Test Connection**. Sencho performs live OIDC discovery against your Issuer URL and reports success or failure next to the button within a few seconds, before you attempt a real sign-in.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-sso/test-connection-success.png" alt="The Custom OIDC card after clicking Test Connection, showing a green checkmark next to the Save and Test Connection buttons." />
|
||||
</Frame>
|
||||
|
||||
A green check confirms Sencho reached the Issuer URL and parsed a valid OIDC discovery document. If it fails, see [If something goes wrong](#if-something-goes-wrong) below before continuing.
|
||||
</Step>
|
||||
<Step title="Sign out and sign in through SSO">
|
||||
Log out of Sencho. The login page now shows your provider's button under **Or continue with**, using the Display Name you set.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-sso/login-page-sso-button.png" alt="The Sencho sign-in page with the normal username and password fields, a divider labeled 'Or continue with', and a Corporate SSO button below it." />
|
||||
</Frame>
|
||||
|
||||
Click it. Sencho redirects you to your identity provider's own login page.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-sso/idp-login-redirect.png" alt="Keycloak's hosted sign-in page, reached by redirect from the Sencho login button, showing Username or email and Password fields." />
|
||||
</Frame>
|
||||
|
||||
Sign in with a directory account that has never logged into Sencho before. On success, the identity provider redirects back and you land on the Sencho dashboard, already signed in.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Verify it worked
|
||||
|
||||
Check from two places, since either alone only shows one side of provisioning.
|
||||
|
||||
**Your own profile menu.** Click your avatar. It shows the username from your identity provider, a **Viewer** role pill (the Default Role from step 2), and a **Community** tier pill, confirming you're signed in as a newly provisioned account, not your admin session.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-sso/profile-badge-viewer.png" alt="The profile dropdown for the newly signed-in SSO user, showing the username, a Viewer role badge, and a Community tier badge, with Settings, Documentation, Open New Issue, and Log Out below." />
|
||||
</Frame>
|
||||
|
||||
**The admin Users list.** Sign back in as an admin (local password, or an SSO account you've mapped to Admin) and open **Settings → Access → Users**. The new account appears in the table with today's date under **Created** and the same **Viewer** role, confirming the server-side record matches what the user saw.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/tutorials/set-up-sso/users-list-provisioned.png" alt="The Users table under Settings, Access, Users, with a row for the newly auto-provisioned SSO user showing role Viewer and today's date under Created." />
|
||||
</Frame>
|
||||
|
||||
On every later sign-in, this same account is reused. Its email and role are re-synced from the identity provider each time, so promoting or removing someone from your directory takes effect on their next login.
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
**Test Connection fails immediately with an HTTPS-related error.** The Issuer URL almost certainly starts with `http://`. Sencho's discovery step requires HTTPS with a certificate it trusts; put a reverse proxy with a valid certificate in front of your identity provider (see the Warning above) and update the Issuer URL, then test again. If the URL is already `https://`, check that it doesn't include the `/.well-known/openid-configuration` suffix and that it exactly matches the `issuer` value your provider's own discovery document reports.
|
||||
|
||||
The [SSO troubleshooting section](/features/sso#troubleshooting) covers issuer mismatches, wrong-username-on-first-login, and the redirect URI error message from the identity provider side.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="SSO & LDAP Authentication" icon="key" href="/features/sso">
|
||||
Every provider (including LDAP), role mapping, SSO-only mode, and full troubleshooting.
|
||||
</Card>
|
||||
<Card title="Two-Factor Authentication" icon="shield-check" href="/features/two-factor-authentication">
|
||||
Require a TOTP code on every SSO sign-in for a stricter posture than SSO alone.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
Reference in New Issue
Block a user