Files
sencho/docs/features/editor.mdx
T
Hambrook a6eaea0f54 feat(editor): create the stack .env from the editor on first save (#2097)
* feat(composeEditor): create .env file on demand when saved instead of relying on doing so via Files

* fix(editor): scope .env creation to the editor and adopt the resolved path

- PUT /:stackName/env creates the default .env only when the request carries
  create=1 and the stack exists; every other caller keeps the handled 404, so a
  Fleet Secrets push to a stack with no env file never creates one.
- The create response returns the resolved path; the editor adopts it, so the
  next save targets the file directly instead of failing the allowed-file check.
- Deep links to /env open the env tab on a stack with no env file.
- Mobile always shows the compose/.env toggle and the creation notice.
- Backend and frontend tests pin the create path, the canonical second save,
  the no-flag 404, the deep-link behavior, and the notice; docs, the guardrails
  tutorial, and the mobile screenshot are updated.

* fix(editor): make the first env save exclusive and pin the confirmed overwrite

Audit follow-up on the create-on-first-save path:

- PUT /:stackName/env?create=1 now creates exclusively with open(..., 'wx').
  An existing target answers the same 412 conflict the optimistic-concurrency
  path uses, with its current content and path, so a stale editor that still
  believes no env file exists can never overwrite one it has not read.
- The editor adopts the conflict path, and its confirmed overwrite retries with
  create=1&force=1&file=<path>, pinned to the file the dialog named and
  re-validated against the allowlist.
- With project env files configured, the create targets the first configured
  file (the one Compose reads), derived from the shared env resolver instead of
  a second copy of the fallback; a configured path that resolves outside the
  stack directory is refused with 409 rather than written.
- The conflict read repeats the containment check and refuses to follow a link,
  so the conflict payload cannot be used to read outside the compose root.
- A directory target maps to 409 instead of an opaque 500.
- Tests cover the conflict instead of an overwrite, the forced retry, the
  pinned retry, the configured target with a stray .env untouched, the escaping
  config refused, force ignored on the non-create path, and the frontend
  conflict, decline, and fallback flows.
- Docs restore the compose row's "Always available." note, describe the
  creation target and the no-overwrite guarantee, and make the notices generic.

* fix(editor): guard the env create against older nodes and unknown state

Audit follow-up on the create-on-first-save path:

- The non-forced create request now carries If-Match: W/"0". This build's
  exclusive create ignores it; a node that predates create-on-save treats it as
  a normal conditional write, so an existing env file answers 412 with its
  content instead of being overwritten, and a missing file keeps the handled
  404.
- A failed stack load clears the previous stack's env state instead of leaving
  a stale "no env file" assertion that the next save could act on.
- A failed env inventory or content read is tracked as unknown, shows a
  failure notice, locks the buffer, and refuses the save; it is no longer
  presented as an empty file.
- Declining the create conflict on a node whose 412 names no path keeps the
  conflicted content and refreshes the inventory under the current load's
  signal; a failed refresh reports the failure instead of a false reload.
- Save & Deploy and Save & Pull on an untouched empty env tab no longer create
  an empty file; the deploy proceeds.
- The exclusive create opens with O_NOFOLLOW so a planted symlink cannot be
  followed, and a failed write's leftover is removed only when the path still
  names that same file (inode and size match).
- Tests cover the older-node precondition, concurrent creates, a directory
  target, escaping and dangling symlinks, the write-failure cleanups, the
  unknown-state refusal, the conflict-retry copy, and the deploy skip.

* fix(editor): leave create mode on older nodes and return 409 for path conflicts

Audit follow-up:

- A create-mode 200 without envPath is the normal response of a node that
  predates the envPath field. The editor now refreshes the env inventory in
  that case, so it leaves create mode and later saves are normal conditional
  writes with a fresh ETag instead of prompting for an overwrite every time.
- The recovery refresh uses the load signal captured before the PUT, so a
  stack switch during the request aborts it and the new load's state is not
  repainted.
- PUT /:stackName/env maps INVALID_PATH and SYMLINK_ESCAPE from the create
  path to 409, so a symlink at the target is a recoverable conflict instead of
  an opaque 500.
- Docs: deep-links no longer says the created file is always the default .env.
- Tests: the older-node create, confirm and overwrite sequence asserts the
  editor leaves create mode and the next save is a conditional write; a route
  test covers an in-root symlinked .env returning 409 without leaking content.
- Comment cleanups from the audit.

* fix(editor): do not paint a create response that lands after a stack switch

Audit follow-up:

- saveFile checks the load signal captured before the PUT (opLoadSignal) in
  the 412 and success paths: a response landing after the editor switched
  stacks now returns without adopting the old stack's envPath, ETag, or
  buffer state and without prompting, so the new stack's editor state stays
  intact.
- Tests: two hook tests switch stacks mid-PUT, for both the success and the
  conflict response, and assert no adoption and no prompt.
- Docs: node-compatibility no longer claims every older node detects the
  conflict (nodes before 0.87 cannot) and notes that the confirmed-overwrite
  prompt does not repeat.
- Comment: createTarget wording matches the resolver's containment filter.

* fix(editor): create missing parent directories for a configured env file

Audit follow-up:

- A configured project env file can point into a subdirectory that does not
  exist yet; the exclusive open failed with ENOENT and the route answered an
  opaque 500. The create now makes the missing parents inside the checked,
  stack-bounded path and retries the exclusive open once, so the first save
  creates the file Compose reads at its configured path.
- The retry re-runs the containment check and still loses to EEXIST if another
  actor created the file meanwhile.
- Tests: a service test for a nested missing-parent target and a route test
  that configures config/prod.env with no config directory, creates it, and
  saves again through the returned path.

* fix(editor): report a vanished stack on env create instead of resurrecting it

Audit follow-up:

- Drop openExclusiveCreatingParents and its recursive mkdir. The state it
  served (a configured project env file in a missing subdirectory) cannot be
  produced by the API: PUT /project-env-files rejects names with "/" or "\"
  and requires the file to exist, so no product path reaches that branch.
  The only reachable ENOENT is a stack directory deleted between
  requireStackExists and the exclusive create; recreating it left the deleted
  stack visible only as a hidden folder holding the env content and blocked
  the stack name.
- Restore the direct exclusive open and map ENOENT in the route catch to the
  handled 404 Stack not found, so the race is reported and nothing is written
  or recreated.
- Replace the two tests for the unreachable nested target with regression
  tests: the service refuses a target under a missing stack directory and
  creates nothing; the route answers 404 Stack not found when the directory
  vanishes before the create (real service ENOENT path) and does not recreate
  it.

* fix(editor): recover when the create conflict target is deleted mid-dialog

QA follow-up:

- The confirmed create retry pins the file the conflict named. When that file
  is deleted before the operator confirms, the route rejected the pinned path
  with a 400 because it was no longer a present env source, and the editor
  (which had already adopted the path) failed every later save the same way.
  The create branch now accepts the pinned path when it is the resolved create
  target, so the retry recreates the file; any other pinned path keeps the
  scoped rejection.
- Declining a create conflict now reloads the env state instead of trusting
  the 412 payload: if the file was deleted meanwhile, the editor returns to
  create mode instead of adopting a path that no longer exists.
- Save failures toast the server's error text (404 vanished stack, 409
  unusable path, 413 too large) instead of the raw JSON envelope.
- The save-refusal toast for an unreadable env state uses the same wording as
  the editor notice; the file list may have loaded while the content did not.
- A viewer with no env file sees "This stack has no environment file yet."
  instead of an unexplained empty read-only tab; editors keep the create
  promise.
- The route test for a vanished stack restores its spy in finally.

---------

Co-authored-by: Anso <dev@anso.codes>
2026-10-08 12:12:26 -04:00

277 lines
26 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Editor
description: The per-stack cockpit. Run, restart, update, and update compose files; stream logs; exec into a container.
---
Selecting a stack in the sidebar opens the editor cockpit, a two-column workspace for everything you do to a single stack. The left column is the **Command Center** with the stack's identity, action bar, container list, and log stream. The right column starts on the **Anatomy panel**, a read-only summary of the compose file; clicking **Edit compose** swaps it for an editable Monaco workspace with `compose.yaml`, `.env`, and **Files** tabs. Creating an empty stack lands you there automatically.
<Frame>
<img src="/images/editor/editor-overview.png" alt="Editor cockpit showing the Command Center and live logs on the left and the Anatomy panel on the right" />
</Frame>
## Command Center
The top card on the left holds the stack's identity and primary controls.
<Frame>
<img src="/images/editor/command-center.png" alt="Command Center card with breadcrumb, stack title, running status pill, action bar, and the plex container row with live stats" />
</Frame>
| Element | What it shows |
|---------|--------------|
| **Breadcrumb** | `<node> › stacks › <stack>` rendered in caps. Confirms which node the stack lives on. |
| **Stack title** | The stack name in display italic. |
| **Status pill** | Aggregate state: `running · healthy`, `running · starting`, `running · unhealthy`, plain `running` (no healthcheck defined), or `exited` (any non-running state, including stopped, created, and paused). The dot pulses while the stack is running. |
### Stack action bar
The action bar runs every state transition for the whole stack. The primary buttons (**Start**, **Restart**, **Stop**, **Take down** when running, **Update**) require the `stack:deploy` permission; the **Delete** entry in the kebab dropdown requires the `stack:delete` permission. The bar still appears when only **Delete** is authorised so the operator has a way to remove the stack.
<Note>
If a stack is the Sencho instance you are currently signed into, stop, take down, delete, update, and rollback stay protected: clicking them opens a **Sencho instance protected** dialog. Eligible admins can apply on-disk Compose changes with **Save &amp; Reapply** in this editor (same recreate procedure as Fleet Node Updates), or open **Fleet → Node Updates**. To manage the stack as a normal stack, move Sencho's compose project outside `COMPOSE_DIR`.
</Note>
| Button | Behavior |
|--------|----------|
| **Start** / **Restart** | A single button that becomes **Restart** when at least one container is running and **Start** otherwise. |
| **Stop** | Stops every container in the stack. Hidden when nothing is running. |
| **Take down** | Shown when at least one container is running. Opens a confirmation dialog, then runs `docker compose down`. Removes containers and compose-created networks while keeping the stack directory. Optional volume removal is offered when the active node supports it. On stopped stacks, use the sidebar context menu or `⌘↓` / `Ctrl+↓`. |
| **Update** | Pulls fresh images and reapplies the compose file. |
| **More actions** (`⋮`) | Opens a dropdown with secondary actions. |
The kebab dropdown carries:
- **Rollback `<timestamp>`** restores the previous deployment using the stored snapshot. Visible only when a backup exists.
- **Scan config** runs Trivy against the compose configuration and surfaces misconfigurations inline. Visible only when Trivy is reachable. Admin role required.
- **Delete** stops the stack and removes its compose directory. Requires the `stack:delete` permission.
## Containers list
Below the action bar, the **CONTAINERS** section lists every container the stack defines. Each row carries a status badge, identity, port mapping, action buttons, and live stats.
<Frame>
<img src="/images/editor/containers-list.png" alt="Container row showing the green check status badge, container name, uptime, port mapping with an inline open link, action buttons, and CPU MEM and NET I/O stats with sparklines" />
</Frame>
| Element | Detail |
|---------|--------|
| **Status badge** | `✓` (green, healthy or running), `✗` (red, exited or unhealthy), `…` (yellow, starting). |
| **Name** | The Docker container name; falls back to the first 12 characters of the container ID. |
| **Uptime / state** | `up 2h 15m` for running containers, the raw state for everything else. |
| **Healthcheck label** | `healthy`, `unhealthy`, or `starting`, only when a healthcheck is defined. |
| **Port mapping** | The first detected web-UI port, formatted `host → container/proto`. The mapping itself is a link that opens the service in a new tab, with a **Copy URL** button beside it. The address uses the active node's host and switches to `https` for port 443. |
| **Action buttons** | **Image source links**, **View logs**, **Inspect image**, **Open bash shell** (admin only), **Service actions**. Per-container image references and registry links live here, not in the header. **Inspect image** opens the same image detail sheet as Resources; Used by shows the current stack only from this entry point. |
| **Live stats** | CPU, memory, and net I/O with rolling sparklines. Only rendered while the container is running. Stats refresh on the same 1500 ms cadence as the dashboard. |
### Service actions
When the container has a compose service name attached, an extra `⋮` button appears at the right of the row. It opens **Restart service**, **Stop service**, or **Start service** depending on the current state. Service actions affect a single service; for full-stack operations use the action bar above.
### Multi-container stacks
Stacks with more than one container gain a summary strip above the container list: a total count plus `up`, `paused`, and `unhealthy` counts (only the non-zero ones show). Two view controls sit beside it:
- **Compact / Detailed** toggles whether the CPU/Mem/Net sparkline grid renders under every container row. Detailed is the default for a single-container stack; multi-container stacks default to Compact.
- **Expand containers** collapses the Logs pane so the container list fills the whole left column. Expanding containers and expanding logs (below) are mutually exclusive; opening one closes the other.
## Anatomy panel
The right column shows the **Anatomy panel** by default: a read-only summary of the compose file alongside a scrollable tab row for other stack views.
<Frame>
<img src="/images/editor/anatomy-tabs.png" alt="Anatomy panel header strip showing the Anatomy, Activity, Doctor, Drift, Dossier, Environment, Networking, and Storage tab row with Files and Edit compose shortcuts on the right" />
</Frame>
The tab row always shows four tabs: **Anatomy**, **Activity**, **Drift**, and **Dossier**. When Compose Doctor is available on the node, **Doctor** sits between Activity and Drift. Four more capability-gated tabs appear when the active node advertises them.
| Tab | Always present? | What it shows |
|-----|----------------|--------------|
| **Anatomy** | Yes | Read-only compose file summary. |
| **Activity** | Yes | Operational event timeline for this stack. See [Stack Activity](/features/stack-activity). |
| **Doctor** | When `compose-doctor` capability is present | Preflight check results grouped by severity. The tab gains a red dot for blocker findings and an amber dot for high-risk findings. See [Compose Doctor](/features/compose-doctor). |
| **Drift** | Yes | Live comparison of the declared compose against the running containers. See [Stack Drift](/features/stack-drift). |
| **Dossier** | Yes | Exportable Markdown of the anatomy combined with operator notes. See [Stack Dossier](/features/stack-dossier). |
| **Environment** | When `env-inventory` capability is present | Variable inventory across all env files, with status for each variable. See [Environment Guardrails](/features/environment-guardrails). |
| **Compose Labels** | When `container-label-inventory` capability is present | Declared Compose labels vs runtime container labels per service. See [Docker Label Audit](/features/docker-label-audit). |
| **Networking** | When `compose-networking` capability is present | Port exposure summary per service with intent classification. See [Compose Networking](/features/compose-networking). |
| **Storage** | When `compose-storage` capability is present | Mount inventory with portability assessment and snapshot coverage. See [Compose Storage](/features/compose-storage). |
The **Files** shortcut and the **Edit compose** button sit at the right end of the strip and stay available regardless of which tab is active.
The Anatomy tab lists:
- **services** as chips
- **ports** in `host → container/proto` rows, grouped by service when the stack defines more than one
- **volumes** in `host → container` rows
- **restart** policy
- **env_file** paths and a count of variables defined in each
- **network** and its driver
- **source** with `git · <host>/<repo>#<branch>` when the stack is linked to a Git repository, or `local` when it is not. Clicking the row opens the Git source dialog. A pulsing brand-color dot means an upstream change is queued.
When an image update is available, or the stack declares services with a local `build:` section, an inline banner appears at the top of the panel. Registry updates follow version-bump severity: `safe to apply` (patch), `review recommended` (minor), `breaking changes possible` (major), or `review required` when the bump cannot be classified. Build-only stacks show **Rebuild available** with a **Rebuild & Update** button. The banner runs the same operation as the action bar's **Update**; it is hidden for roles that lack the `stack:edit` permission and when the bump is flagged as blocked.
If the most recent post-deploy vulnerability scan for this stack failed, partially failed, or was skipped, a dismissible amber banner appears below the update banner (`Last post-deploy scan failed.`, `...partially failed.`, or `Post-deploy scan did not run.`). See [Vulnerability Scanning](/features/vulnerability-scanning) for what triggers a post-deploy scan.
<Frame>
<img src="/images/editor/scan-status-banner.png" alt="Anatomy panel showing a post-deploy scan failed banner below the compose facts" />
</Frame>
## Editor mode
Clicking **Edit compose** in the Anatomy strip opens an editable Monaco workspace. Creating a new empty stack does the same automatically.
<Frame>
<img src="/images/editor/editor-edit-mode.png" alt="Editor in edit mode with the compose.yaml tab selected, the Git Source button, the Save and Deploy split button, and the close button visible" />
</Frame>
The editor card has three tabs:
| Tab | Purpose |
|-----|---------|
| `compose.yaml` | YAML editor with syntax highlighting. Always available. |
| `.env` | Environment file editor with syntax highlighting. When the stack has no env file yet, the first save creates the file Compose will read: a configured project env file, or the default `.env`. |
| **Files** | Browseable directory tree for the stack folder. See the dedicated [Stack File Explorer](/features/stack-file-explorer) page for permissions, upload caps, and protected-file behavior. A full-screen toggle appears next to **Close editor** while this tab is active, collapsing the left column so the file tree and editor use the full width. |
Above the editor area, the toolbar carries:
- **Git Source** opens the Git source dialog. A pulsing brand-color dot appears next to it when an upstream change is waiting to be applied.
- **Save & Deploy** split button (shown as soon as the compose editor opens for operators with `stack:edit`).
- **Close editor** (`X`) discards any unsaved compose and env edits and returns the right column to the Anatomy panel.
When you switch to the `.env` tab and the stack defines more than one env file, a dropdown appears next to the tab strip so you can pick which file to edit. The dropdown locks while you have unsaved edits so switching files cannot drop them. Sencho discovers env files from the `env_file:` entries in `compose.yaml`; if none are declared, it falls back to a `.env` in the stack directory.
The `.env` editor renders a teal banner above the textarea reminding you that variables defined there are available for substitution in `compose.yaml` (`${VAR}`), and that passing them into containers requires an `env_file:` entry on the service. When the stack has no env file yet, the same banner adds that saving creates the environment file in the stack directory. Creation never overwrites: if the file appears between opening the tab and saving, Sencho asks before replacing it, and declining reloads the file that is on disk.
### Save options
As soon as the compose editor opens, the toolbar shows a split button. The primary action is **Save & Deploy** for ordinary stacks. On Sencho's own Compose-managed stack, eligible admins see **Save & Reapply** instead. The dropdown chevron reveals three more actions.
| Action | Effect |
|--------|--------|
| **Save & Deploy** | Writes the file to disk, then runs `docker compose up -d` to apply changes. |
| **Save & Reapply** | Shown only for admins on the Compose-managed Sencho self-stack when reapply is available. Writes the file, then confirms and recreates Sencho from the current on-disk Compose project without selecting a newer image. Same procedure as Fleet **Reapply configuration**. |
| **Save Only** | Writes the file to disk without restarting any containers. Changes take effect on the next deploy or reapply. |
| **Save & Pull Images** | Writes the file to disk, then downloads the stack's registry images to the node. Running containers are left exactly as they are. Services that build from source are skipped and listed in the confirmation; nothing is built. Use it to warm the image cache ahead of a maintenance window, or to stage an update and apply it later. Changes take effect on the next deploy. |
| **Discard Changes** | Reverts the active file (compose or env) to the last saved version. Unsaved edits in that file are lost. |
Downloading images is distinct from the **Pull now** action in the Git source dialog, which fetches the latest commit from the configured repository. **Save & Pull Images** never fetches source and never rebuilds; it only brings registry images onto the node. It is also hidden on Sencho's own Compose-managed stack, whose images update through **Fleet → Node Updates**.
The action runs the same checks every other Compose operation runs before it touches the stack, so it refuses rather than half-applies in two cases. If the stack's repository secrets are SOPS-encrypted, the editor's mutating actions are refused and the stack has to be applied through Git so the secrets are decrypted in a controlled overlay. If **Block deploy on missing required env vars** is on in **Settings → Infrastructure → Stacks** and a required variable is unset, the pull is refused with the same message **Save & Deploy** and **Update** give, and no images are downloaded.
Against a node whose Sencho instance does not include this action, only the download half fails. The file is written before the download is attempted, so your edit is on disk, no image is downloaded, and no running container is touched. The toast reports the image pull as failed and names the refusal rather than its cause. Update that node, then run **Save & Pull Images** again: the second run reads the file already on disk and downloads the images from it. See [Node Compatibility](/features/node-compatibility) for how Sencho handles version differences across a fleet.
The same controls apply to the `compose.yaml` and `.env` editors.
## Diff preview before save
When **Diff preview before save** is enabled in **Settings → Infrastructure → Stacks**, clicking **Save & Deploy**, **Save & Reapply**, **Save Only**, or **Save & Pull Images** opens a side-by-side diff modal before anything is written to disk. The left pane is the on-disk content; the right pane is your unsaved edits with additions highlighted in green. The footer reads `ON DISK → UNSAVED` so the panes are unambiguous.
<Frame>
<img src="/images/compose-diff-preview/diff-modal.png" alt="Diff preview modal showing side-by-side YAML diff with the unsaved version on the right and an ON DISK to UNSAVED legend in the footer" />
</Frame>
Review the diff, then:
- Click the primary button (**Save & deploy** or **Save & reapply** when triggered from that primary action, **Save** when triggered from **Save Only**, or **Save & pull images** when triggered from **Save & Pull Images**) to confirm and write the changes.
- Click **Cancel** to return to the editor without saving.
If there are no unsaved changes the modal is skipped and the save proceeds directly. The toggle is off by default and stored per browser, so each device remembers its own setting.
## On a phone
On a narrow screen the stack opens as a full-screen detail with **Health**, **Logs**, and **Compose** segments instead of the two-column cockpit. The **Compose** segment shows the read-only Anatomy summary; tap **Edit compose** to open a full-screen editor for small, safe changes.
<Frame>
<img src="/images/editor/editor-mobile.png" alt="Mobile compose editor with a Cancel button, the compose/.env toggle with compose selected, a monospace text field showing the compose file, a small-edits note, and Save and Save and Deploy buttons" />
</Frame>
The mobile editor is a lightweight monospace text field rather than Monaco. Tap **compose** or **.env** at the top to choose the file. The file picker is locked while you have unsaved edits so switching files cannot drop them. When the stack has no env file yet, the `.env` view explains that saving creates the environment file. The footer carries the same **Save** and **Save & Deploy** (or **Save & Reapply** when eligible) actions, and a chevron attached to the primary action holds **Save & Pull Images** so the action row never crowds the primary label. Every protection is shared with desktop: the diff preview, save-conflict handling, and the unsaved-changes prompt all behave the same way. **Cancel** leaves the editor and asks before discarding unsaved edits.
A note at the bottom of the editor is a reminder that mobile editing is meant for small corrections such as bumping an image tag or fixing a value. For large compose rewrites, open the stack on a desktop. Editing requires the `stack:edit` permission.
## Logs
Below the Command Center the left column reserves the rest of its height for the **Logs** stream. Two modes are available, toggled by the buttons at the top right of the section.
| Mode | What it shows |
|------|--------------|
| **Structured** | Parses each line for an ISO 8601 timestamp and severity (info, warn, err) and renders rows with filter chips (`all`, `info`, `warn`, `err`) and a counter on the active level. Strips ANSI control codes and auto-follows new lines while you stay at the bottom; scrolling up reveals a **resume follow** button. The buffer is capped at 10,000 lines and the **Download logs** button writes the current buffer to a `.txt` file. |
| **Raw terminal** | A full xterm.js terminal that streams the combined container output verbatim with ANSI color preserved. Press `Ctrl+F` to open the inline find bar. The scrollback is 10,000 lines and a download button is available in the toolbar. |
Both modes connect to the same WebSocket and switch instantly without losing the stream. In Structured mode, an **Expand logs** button next to **Download logs** collapses the Command Center card so the log pane fills the left column; click again (now **Collapse logs**) to bring the Command Center back. This control is mutually exclusive with the container-list **Expand containers** toggle above.
## Log viewer
The **View logs** button on a container row opens a dedicated modal that streams output from a single container in real time using Server-Sent Events. Logs auto-scroll to the bottom as new lines arrive. Close the modal to stop the stream.
## Container exec (bash session)
The **Open bash shell** icon button opens an interactive shell inside a single running container, equivalent to `docker exec -it <id> bash` (or `sh` when the image has no bash). The modal uses xterm.js with color, tab completion, and resize handling.
Right-click copies the selected text, or pastes from the clipboard when nothing is selected. Ctrl+V, Ctrl+Shift+V, and Shift+Insert also paste (Cmd+V on macOS). When Sencho is served over plain HTTP, the browser does not let pages read the clipboard, so right-click opens the browser's own menu instead; choose **Paste** there or use the keyboard shortcut.
<Frame>
<img src="/images/editor/container-exec-modal.png" alt="Bash session modal connected to the plex container running cat slash etc slash os release with a Connected status pill" />
</Frame>
### Access requirements
- **Admin role required.** The button is rendered only for admin users.
- **Container must be running.** The button is disabled for stopped containers, and the backend rejects exec attempts against non-running containers.
- **API token scope.** API tokens with the `read-only` or `deploy-only` scope cannot open exec sessions. Only `full-admin` tokens are accepted.
### Shell selection
Sencho tries `/bin/bash` first and transparently falls back to `/bin/sh` if bash is missing. No user action is needed.
<Warning>
Minimal base images that ship no shell at all (for example `scratch` or fully `distroless`) will not work with container exec.
</Warning>
## Troubleshooting
<AccordionGroup>
<Accordion title="The compose editor is read-only and I can't type">
Click **Edit compose** in the Anatomy panel header to open an editable workspace. If that control is missing, your role lacks the `stack:edit` permission; ask an admin to grant it.
</Accordion>
<Accordion title="Save & Deploy is disabled">
A deploy is already running for this stack. Watch the action bar for the spinning state to clear, then save again.
</Accordion>
<Accordion title='Log viewer shows "Request forbidden by administrative rules"'>
The container list can still load while log viewing fails. The **View logs** stream can show `[Sencho] Error fetching logs:` followed by an HTTP 403 and `Request forbidden by administrative rules`. A Docker socket proxy is blocking `/containers/{id}/logs`. On `linuxserver/socket-proxy`, set `ALLOW_LOGS=1` in addition to `CONTAINERS=1`. Tecnativa-style profiles include logs under `CONTAINERS`. See [Self-Hosting: Docker socket proxy](/operations/self-hosting#linuxserver-socket-proxy-extras).
</Accordion>
<Accordion title="I can't edit compose from my phone">
Open the **Compose** segment in the stack detail and tap **Edit compose**. If that affordance is missing, your role lacks the `stack:edit` permission; ask an admin to grant it. The phone editor is intended for small corrections; for large compose rewrites, open the stack on a desktop.
</Accordion>
<Accordion title='"Container is not running" error when opening a bash session'>
The container stopped between clicking the button and the exec starting. Start the container from the action bar (or the row's **Start service** option) and try again.
</Accordion>
<Accordion title='"Failed to start shell" error when opening a bash session'>
The container has no usable shell at `/bin/bash` or `/bin/sh`. Use an image that includes a shell, or install one in your Dockerfile.
</Accordion>
<Accordion title='Bash session connects but immediately shows "Session ended"'>
The shell process inside the container exited as soon as it started. Check the container logs (the **View logs** button on the same row) for errors; the container's entrypoint may be overriding the shell, or the container itself may be in a crash loop.
</Accordion>
<Accordion title="Bash session is unresponsive">
The WebSocket dropped silently. Close the modal and reopen it to start a fresh session; the bash modal does not reconnect automatically.
</Accordion>
<Accordion title='"Failed to save file" toast appears when I click Save & Deploy'>
The PUT to write the compose file failed (most often a permission or disk-space issue on the node). Save & Deploy stops at the save step in this case, so no deploy is attempted and your editor still holds the unsaved version. Resolve the underlying error on the node, then retry.
</Accordion>
<Accordion title='"Image pull failed" toast appears when I click Save & Pull Images'>
Both halves are reported separately, so this toast means the file **was** written and your edits are safe on disk; the download of its registry images is what failed, usually because a registry is unreachable or a credential was rejected. No running container was touched. The toast quotes the error lines Compose reported, which usually name the service whose image failed. To see which images did arrive, open the progress panel: it streams the full output image by image, and the toast does not carry that per-image detail. Fix the registry problem and run **Save & Pull Images** again.
</Accordion>
<Accordion title='"Image pull failed" toast names a missing API endpoint on a remote node'>
The node does not implement the download itself, which is what a fleet with nodes on different Sencho versions looks like when the node has no copy of this action. The compose file was still written before the refusal, so your edit is on disk, no image was downloaded, and no running container changed. Update that node from the Fleet view, then run **Save & Pull Images** again on it; the second run reads the file already on disk and downloads the images from it. The version a node runs appears next to its name in the node switcher once you have visited that node, and **Settings · Nodes** shows it directly in the connection details.
</Accordion>
<Accordion title="Switching nodes prompts me to discard unsaved changes">
The editor blocks a silent loss of in-progress edits. Click **Cancel** to return to the original node with your edits intact; click **Discard** to abandon them and proceed to the other node.
</Accordion>
<Accordion title='"Sencho instance protected" dialog when I try to stop, update, or delete a stack'>
That stack is running the Sencho instance you are signed into. Stop, take down, delete, update, and rollback stay blocked. Eligible admins can apply Compose edits with **Save &amp; Reapply** in the editor, or use **Fleet → Node Updates**. To manage the stack normally, move its compose project to a directory outside `COMPOSE_DIR`.
</Accordion>
<Accordion title='"Stats unavailable" chip on the container list'>
The stats WebSocket failed to open, or closed unexpectedly. This usually means the Docker daemon on the node is unreachable. Container status, action buttons, and logs continue to work; only live CPU / memory / network rates pause.
</Accordion>
</AccordionGroup>