Anso ca90588e4e fix(gitops): bound GitOps history with a retention window (#2105)
* fix(gitops): bound GitOps history with a retention window

`gitops_history` is insert-only and the reconcile tick appends to it
continuously, so it grew for the life of an installation. A new
`gitops_history_retention_days` setting (default 30, range 1 to 365)
seeds with the other GitOps settings and prunes history from the
monitor's cleanup pass. Drained outbox rows are pruned with the history
they announced; an undrained row gets a grace period past the window
because the boot repair can still drain it from its payload, and a row
still undrained after the grace is counted, logged, and dropped so it
cannot accumulate for ever. The window is instance-local, like the
artifact retry interval beside it: the history being pruned is the one
this instance holds, and the monitor that prunes it runs here.

* fix(gitops): keep each live application's newest history row through the prune

`cleanupOldGitOpsHistory` deleted `gitops_history` purely by age, but
`latestTransitionByApplication` reads `MAX(created_at)` per application and
feeds the "last activity" fact the portfolio and application sheet render. An
application with no history inside the window, because polling is off, a source
is suspended, a node is detached, or a Blueprint is idle, lost every row and
its activity column silently went empty. The prune now exempts each live
application's newest row, so the claim the JSDoc, the PR body and the settings
helper text already make is true.

Scoped to live applications rather than to every application_id: a tombstone in
`gitops_applications` is permanent, so an unscoped exemption would retain one
history row per stack ever retired and leave exactly the growth this setting
exists to stop. A retired application's rows are now pruned in full.

Also folds in three follow-ups from the audit:

- The prune is chunked to GITOPS_PRUNE_CHUNK_ROWS rows per table per pass, so
  a first prune on a long-lived install clears its backlog over several janitor
  ticks instead of holding SQLite's single writer for the whole delete. The
  method reports historyRemaining and outboxRemaining so the debug log says
  whether a pass is still catching up.
- The outbox OR-delete is two statements, so the undrained branch uses
  idx_gitops_settled_outbox_undrained instead of scanning for the drained one.
- The retention tests assert exact counts and exact surviving row ids rather
  than toBeGreaterThanOrEqual over a shared database.

New tests pin each guard: a quiet application's newest row survives every
window, its older rows are pruned, a tombstoned application clears entirely,
an unsettled attempt is still recoverable after a prune both inside the window
and outside it, and a backlog clears in bounded passes with the remainder
reported.

Removing the newest-row exemption turns 10 of the 11 tests red; removing only
the liveness clause turns the tombstone test red.

Docs: the GitOps portfolio page describes the window and its setting, and now
states that the newest entry per application is kept, which is what the code
does.

* test(gitops): drop the unused db handles from the recovery assertions

* fix(gitops): keep a settled history row ahead of its started row

A reconcile attempt stamps both rows with the same time, and the prune broke that tie on a random id, so it could delete the settlement and leave startup recovery to reopen the attempt. Recency now follows insertion order, in the exemption and in each chunk.

Remaining counts run only while developer mode is on, drained outbox deletes use an age index, and the settings copy says the kept row is the newest one for each live application.

* fix(gitops): log the undrained rows a history prune actually deletes

A pass is capped, so the warning was reporting the whole backlog. The
history-window tests now fail if the permission, the page save, the monitor
window, or the getter fallback stops matching the code.

* fix(gitops): prune a superseded unsettled attempt past the window

An unsettled reservation survives past the retention window only while it is that live application's newest row. Creating applications keep that row; detached applications do not.

* fix(gitops): skip the history prune when the retention setting cannot be read

A failed settings read used to fall back to 30 days and delete rows the operator had chosen to keep. The getter now throws, and that pass deletes nothing. A missing or unusable stored value still uses 30.

* fix(gitops): restore the history window when the retention spec fails

The retention spec restored the seeded 30 days as its last statement, not
in a finally, so a failed assertion left the shared instance on 14. The
CI retry then read 14 and failed the fresh-install assertion, turning one
failure into a persistent red.

Everything after the first save now runs inside a try whose finally puts
the window back, so the spec leaves the instance as it found it whether it
passes or fails.

* docs(settings): list the GitOps history window in the retention reference

The Data Retention table listed every other retention window and not the
GitOps one, so an operator checking retention windows did not find it. The
portfolio page described the setting in a paragraph, which is not where
retention windows are looked up.

Adds the row with its default and range, and a line under the table saying
the window is set per Sencho instance and where the control lives.

* fix(gitops): report an unmeasured prune remainder as unknown

The prune reported a remainder of 0 whenever developer mode was off, so
the diagnostic log read "none left" for a table it had never counted. It
now reports null, and the log line says the remainder was not measured.

Three smaller corrections from the same review:

- The diagnostics allowlist omitted the history window, so a support
  bundle could not explain a timeline that had already been pruned.
- The GitOps registry entry described every control as belonging to this
  node, while the retry interval and the history window are instance
  local. The sentence now names no scope it does not have.

* fix(gitops): correct what the history window is documented to govern

The copy said the window governs notifications. It does not: the prune
deletes history rows and their outbox rows, and the bell entries those
outbox rows announce age out under the Notification log window instead. An
operator setting 7 days would have expected their bell to follow.

It also promised to keep the database small, which nothing delivers. The
prune stops old rows from being written; it never reclaims the file.

- The helper now names the record behind the GitOps timeline, points bell
  entries at the Notification log, and says the window stops history from
  growing.
- The settings reference gains the GitOps section it never had, with the
  window under it rather than under a per-node table that ends in a Save
  button the control does not use.
- The portfolio page stops claiming a shorter window never changes what an
  application is doing: a webhook redelivery reads this same record, which
  the same paragraph already said.
- A fraction below one day now clamps to a day instead of flooring to zero,
  which as a cutoff is now and would delete everything not exempt. The only
  caller passes an integer, so this is a guard, not a reachable fix.

* fix(settings): snap a typed number chip to its own step on commit

The chip clamped to min and max but never rounded, so typing 1.5 wrote
"1.5" and every integer setting rejected it at the route with a bare
"Validation failed" toast. Nothing was saved, so no data was at risk, but
the control accepted input it could not store.

Rounding to the control's step fixes that and leaves a genuinely
fractional chip alone: the Docker janitor asks for 0.5 GiB and keeps it.
Clamping still runs after rounding, so a fraction below the floor cannot
round down under it.

The settings reference also listed GitOps last under Infrastructure
while the sidebar renders it third.

* docs(gitops): replace the unsourced window figure with what the code does

The helper and both docs said a window under about 3 days can let a Git
host retry a webhook. Nothing in the code picks three days, and the
dedupe the sentence describes only applies to the direct webhook path.

The text now states the condition instead of a number: a redelivery can
run the work again once the record of its first delivery is pruned. A
provider endpoint keeps its own delivery record, pruned to a fixed depth,
so the window does not affect it, and the sentence says so.

* docs(settings): say where a remote instance's window is set

The reference explained that the window belongs to an instance rather
than a node, but not how to change the window of a remote one, which is
on that instance.
2026-10-11 14:02:03 -04:00

Sencho

Self-hosted Docker Compose management for one machine or a fleet.

Docs · Tutorials · Website · Roadmap · Discussions · Sponsor · Buy Me a Coffee

Latest release Docker Pulls CI CodeQL License Last commit Open issues Website Docs Roadmap


Sencho stack detail view showing the Drift tab for the cloudflared stack

What Sencho is

Sencho is a Docker Compose control plane for DevOps engineers, platform teams, system administrators and homelab users who run services on Compose and need a real operational surface: a graphical interface that does not give up file-on-disk workflows, and the ability to manage more than one machine without SSH gymnastics or a VPN.

It runs as a single container on your hardware and provides a UI for common Compose operations: deploying, editing files, watching logs, restarting containers, browsing volumes, and recovering from failures. Your compose files stay on the host filesystem and remain the source of truth.

Multi-node was part of the architecture from the start, not bolted on later: every Sencho instance is the same autonomous node, whether it runs alone or as one of many in a fleet. To manage another machine, you install a second Sencho on it and connect them with a long-lived API token; the primary dashboard then acts as an authenticated HTTP and WebSocket proxy across your fleet. Use TLS, a VPN, or a private network for any untrusted link. Each node still uses its local Docker socket (see Quick start), but Sencho does not require SSH and does not expose a remote Docker socket on the network. For nodes behind NAT or strict firewalls, the Pilot Agent establishes a single outbound WebSocket tunnel to the primary, so the remote host opens no inbound port at all.

Sencho is free, open-source software under AGPLv3. Everything below is included in the Community tier with unlimited nodes and users.


Capabilities

Stacks

  • Full Compose lifecycle: create, deploy, restart, stop, take down, pull
  • Atomic deployments with automatic rollback on failure
  • Monaco editor with diff preview before save and one-click rollback to any prior deploy
  • Health-gated updates that hold a rollout until health checks pass, with stalled-update detection and in-app recovery
  • Git-sourced stacks pulled and synced from any repository, with ordered multi-file Compose
  • File explorer for compose, env, and supporting files, with move and rename across directories
  • Drift detection that compares running containers against the effective Compose model and flags exactly what changed
  • Environment and secrets guardrails that inventory every variable a stack uses and flag missing or duplicate values, without ever exposing a value
  • Storage portability checks that show whether a stack's mounts can move cleanly to another node before you move it
  • Compose Doctor preflight checks that catch compose problems before deploy
  • Stack labels for grouping and bulk operations
  • App Store with LinuxServer.io templates by default, or any custom Portainer-compatible registry

Observability

  • Aggregated log search and stream across every container in the fleet
  • Live container stats, health checks, and image-update notifications on a configurable cadence, with links from each image to its registry and source
  • Threshold alerts for CPU, memory, and network
  • Read-only audit log of every action, with a 14-day recent-activity window
  • Network topology view of containers, networks, and nodes
  • Documentation-drift flags when a stack dossier diverges from the running stack

Fleet

  • Multi-node management via authenticated HTTP and WebSocket proxy
  • Fleet view with grid and topology layouts
  • Fleet snapshots of compose and env across the fleet
  • Fleet Federation: cordon nodes and pin Blueprints to specific hosts
  • Fleet Actions: bulk label operations, fleet-wide stop-by-label, and fleet-wide prune
  • Fleet Dossier: export the whole fleet as a single browsable Markdown archive
  • Fleet Secrets: author environment-variable bundles once, push them to labeled nodes' stacks, with an audit trail of every change
  • Fleet Sync: push scan policies, CVE suppressions, and misconfig acknowledgements from a control instance to its replicas
  • Docker Label Audit across every node, for labels that drive external automation
  • Remote updates: pull the latest image and recreate any node in the fleet from the Fleet view, no SSH session required
  • Node labels and grouping
  • Pilot Agent for nodes behind NAT or strict firewalls
  • Node compatibility checks before deploying

Automation

Security

Operations


Before you install

Sencho talks to Docker through the host's /var/run/docker.sock. Mounting this socket grants Sencho the same privilege as sudo docker on the host. This is the same model used by Portainer, Dockge, Komodo, and other Compose dashboards. If your threat model requires stricter isolation, see running with a non-root container user and front Sencho with a reverse proxy that enforces authentication.

Quick start

Sencho runs in a single container.

services:
  sencho:
    image: saelix/sencho:latest
    container_name: sencho
    restart: unless-stopped
    ports:
      - "1852:1852"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./data:/app/data
      # 1:1 Compose Path Rule: the host path MUST match the container path
      - /opt/docker:/opt/docker
    environment:
      - COMPOSE_DIR=/opt/docker
      - DATA_DIR=/app/data
docker compose up -d

Open http://your-server:1852 and create your admin account.

Always front Sencho with a TLS-terminating reverse proxy in production. See the self-hosting guide for hardening, environment variables, and reverse-proxy examples.

Run with docker run instead
docker run -d --name sencho \
  -p 1852:1852 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v sencho_data:/app/data \
  # 1:1 Compose Path Rule: the host path MUST match the container path
  -v /opt/docker:/opt/docker \
  -e COMPOSE_DIR=/opt/docker \
  saelix/sencho:latest

For the full walkthrough, see the quickstart guide.


Adding remote nodes

To manage a second machine, install Sencho on it the same way, then add it from the primary dashboard with its URL and a long-lived API token. The primary proxies authenticated HTTP and WebSocket requests to the remote instance. The remote node does not run SSH for Sencho, does not expose its Docker socket on the network, and does not run a separate agent process. The local Sencho on each node manages its own Docker through the standard socket mount described in Quick start. Nodes behind NAT or strict firewalls can opt into the Pilot Agent for outbound-only connectivity.

See the multi-node guide for the full token-bearer flow.


Screenshots

Stacks Editor
Fleet Logs
Security overview Blueprints and drift
Scheduled Operations Compose Doctor
Same screens in OLED
Stacks Editor
Fleet Logs
Security overview Blueprints and drift
Scheduled Operations Compose Doctor
Same screens in Light
Stacks Editor
Fleet Logs
Security overview Blueprints and drift
Scheduled Operations Compose Doctor

Telemetry and data handling

Sencho does not emit telemetry, analytics, or crash reports, and makes no outbound calls to Sencho-controlled endpoints. Stack metadata, container inventory, and user activity never leave your instance.


Admiral

Admiral is Studio Saelix's paid business assurance plan on top of everything in Community: Hardened Build, Recovery Vault (managed off-site snapshots), priority support, and governance depth (LDAP / Active Directory, full audit log export and anomaly detection, and related organizational controls). Built-in RBAC (five roles and scoped assignments) is included on Community. AWS ECR registry credentials currently require Admiral as well; that access rule is temporary availability, not the reason Admiral exists. See sencho.io/pricing for current plan details.


Documentation, community, and license


Contributors

S
Description
Self-hosted Docker Compose management platform. Great for homelabs, small DevOps teams, and platform engineers.
Readme AGPL-3.0 269 MiB
Languages
TypeScript 99.1%
JavaScript 0.6%
CSS 0.2%