ADR 0015 — Read-only engine and resource insights
Status: accepted.
Context
The panel manages Compose stacks and, since ADR 0014, observes every container
through a read-only ContainerInspector. An operator still cannot answer three
concrete questions from one place: what is running, what consumes resources, and
what needs attention. The host-level view (load, uptime, swap, filesystems,
package updates) is unavailable because the panel runs in a container and does
not see the host namespaces; that wider view would need an optional host
component and belongs to a separate decision.
Three Docker commands give useful, already-available facts without any host agent:
docker info— static facts about the machine running the daemon (OS, kernel, architecture, CPU count, total memory, engine version, object counts).docker stats --no-stream— per-container CPU, memory, network and block I/O, and PIDs, as Docker's CLI reports them.docker system df— Docker-managed storage (images, containers, volumes, build cache) and how much is reclaimable.
Their costs differ sharply. info and stats --no-stream are cheap-ish and
bounded; docker system df walks image/container/volume filesystems and the
Docker documentation warns it can be expensive on hosts with a lot of data. A
timeout bounds the client wait, not the work already started inside the daemon.
Two mistakes must be avoided:
- Turning the panel into a monitoring system. There is no history, no series store, no alerts and no daemon collecting in the background.
- Presenting engine facts as host health.
docker infodescribes the machine that runs the daemon (possibly a VM);docker system dfmeasures Docker's own data, not free partition space.
Decision
- Extend
ContainerInspector(src/containers.ts) with three read-only methods:info(),stats()anddiskUsage(). They are lower-level, non-Compose Docker calls, so they belong to the same boundary aslist,inspectandlogs(ADR 0014); the Compose lifecycle contract stays inComposeProvider. Exactly these three methods are added: this iteration does not turn the inspector into a generic monitoring object. Host health and maintenance (the later "host insights" work) must introduce their own boundary rather than keep growing this one. - Fixed executable and explicit argument arrays, never a shell.
docker info --format {{json .}}docker stats --no-stream --no-trunc --format {{json .}}docker system df --format {{json .}}There is no user-supplied option and no mutation.
- A whitelist projection for
info. TheInfoobject is large and may carry registry, proxy and environment configuration; only named fields are extracted (insights.ts). The raw object is never retained, logged or returned, exactly asdocker container inspectis projected in ADR 0014. - Honest semantics.
- CPU is summed across a stack's containers and may exceed 100 % (Docker's scale is per core). It is reported as a cumulative figure, never as a share of total server capacity, and a usage bar is not clamped to 100 %.
- Memory is summed in bytes; a memory limit and a memory percentage stay
per-container. Meters derived from
MemPercare never summed. - Network and block I/O are cumulative counters, not rates, and stay at the container level in this iteration; they are labelled "I/O", never "/s".
- The memory semantics of
docker stats(the CLI subtracts page cache on Linux) are preserved: values are taken from the CLI output, never recomputed from the raw API.
- Collection cost is respected.
infoandstats --no-streamare collected on page open and on an explicit manual refresh.docker system dfis never collected automatically; it is read on demand, behind a short in-memory cache (about five minutes). A future live refresh must never trigger it.
- Unavailable is not zero. Each collection is independent and its result is
nullwhen it could not be obtained, distinct from an empty result. Ifstatsfails whileinfosucceeds, the page keeps what it has. The loader swallows only expected operational failures (unavailable, timeout, unparsable output); a programming error still propagates. - Stack aggregation reuses ADR 0014 classification. External and standalone containers are visible, but their usage is never added to a managed stack on a name resemblance; only containers classified as Managed contribute to a stack total.
- "Needs attention" is a snapshot of the current state, from data already in
the container list:
unhealthyandrestarting, the problems visible now. An exited container whose status shows a non-zero exit code (derived from thedocker psStatustext so no extrainspectis needed) is listed apart, folded under it as "Stopped with an error", and not counted: finished jobs such as CI or build steps often end that way. (Amended in 0.3.2; it was counted as attention before.) It carries no history and no alerting.
Consequences
- An operator can see what runs, what consumes, and what looks wrong, on one read-only page, without installing anything.
- The panel's Docker surface widens again. It stays read-only and bounded, but the guarantee is application-level: a full panel compromise already has root-equivalent access through the socket. Accepted for the single-trusted-administrator threat model.
- Host CPU/RAM/load/uptime, swap, zombie processes, filesystems and package updates remain out of scope. They require a separate, optional, strictly allow-listed host component and its own ADR.
- Registry update checks (comparing a remote digest with the local one) are a distinct item and are not part of this decision.
- No history, no series database, no alerting and no background collection are introduced; the in-memory disk cache disappears on restart.