feat(dozzle): live log viewer — the first container-logs@1 provider (0.1.0) - #160
Conversation
`container-logs@1` has shipped in Hola since 0.11.0-rc.1 — the redacting Docker façade, the grant, the consent step, the `sh.hola.*` labels, the tests — and nothing has ever called it. This is the app that does. Dozzle is a good first provider precisely because it knows nothing about Hola. It reads `DOCKER_HOST` like any Docker tool; declaring `provides: ["container-logs@1"]` is the entire integration, and the server does the rest: injects its own `hola-docker-proxy` sidecar holding the socket read-only, points `DOCKER_HOST` at it, and answers only GET on the container list, logs, events, a redacted inspect and the probes. The compose file is mostly comment explaining what it deliberately does NOT contain — no socket mount, which the validator would reject anyway. The catalog vocabulary had to catch up first: `schemas/manifest.schema.json` and `bin/validate-manifest.mjs` knew only `auth@1`, `backup@1` and `push@1`, so the contract was undeclarable here even though the server has implemented it for three releases. Both now carry `container-logs@1`, and `accepts` is split onto its own `acceptableContractRef` enum so naming an implicit contract as an acceptor is a CI error rather than something Hola silently drops. Two Dozzle features are disabled rather than left to fail: container actions and shell both need Docker API writes the grant withholds. Stats and `/info` are outside it too, so the UI degrades to logs without resource graphs — the first real finding about where that allowlist sits. Identity comes from the gate: `auth.mode: forward-auth` with Dozzle's `forward-proxy` provider reading Authentik's headers, so the person who signed in is the person Dozzle shows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vck5KSX2CLxhohx14nb5Sh
A release version only publishes on merge, so the package cannot be installed anywhere until it lands — which is the wrong order for the first app to use a contract nothing has ever exercised. Cut it as a pre-release instead: CI publishes ghcr.io/try-hola/dozzle:0.1.0-beta.1 from this PR, it can be installed by OCI ref on a disposable VM, and it graduates to 0.1.0 once the container-logs@1 grant is proven end to end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vck5KSX2CLxhohx14nb5Sh
VM test resultsInstalled on a disposable VM (Hola 0.11.0-rc.6, Authentik mode) alongside an uptime-kuma neighbour. The package works, and it found three platform bugs — fixed in try-hola/hola#468, which this now depends on. What the package proved
Consent turning out to be mandatory rather than optional is a better guarantee than I described in the PR body: there is no unprivileged half-installed provider state to fall into. The three bugs (all shape, not policy)
After the fix: Envelope still holds403 on: stats, One thing to decide
Merge ordertry-hola/hola#468 → a release carrying it → this. On an older server the package installs and the proxy appears, but dozzle will crash-loop. 🤖 Generated with Claude Code |
The container-logs@1 proxy has shipped since 0.11.0-rc.1 with no provider to exercise it. Packaging one (Dozzle, try-hola/apps#160) and installing it on a VM found three ways the envelope refused the clients it exists for. All three were shape, not policy — nothing here widens what the grant discloses. 1. HEAD was refused. Docker's own client pings with `HEAD /_ping` before anything else, so a GET-only allowlist rejected every standard client on its first call, and the client reported it as "Could not connect to any Docker Engine" — not as a refusal. HEAD is now allowed wherever GET is: it returns headers and no body, so it reveals strictly less than the GET it mirrors. 2. GET /info was refused outright. A client on Docker's SDK calls it to decide an engine is really there and exits without it. It is now rebuilt from an allowlist, the same way inspect is: what identifies and sizes the engine survives (name, version, OS, arch, cpu/memory, container counts) and everything describing how the host is configured does not — RegistryConfig, Labels, Plugins, DockerRootDir, SecurityOptions, Swarm, and above all HttpProxy/HttpsProxy, which routinely embed credentials. 3. The redacted inspect omitted HostConfig, Mounts and NetworkSettings entirely. A real daemon always returns them, so clients walk them without checking — Dozzle segfaults on HostConfig.PortBindings. Dropping the field denied the client, not the data. They are now present but empty: no host port map, no bind sources, no network topology, and the same nothing on the wire, in a shape an SDK client can parse. Verified on a disposable VM against the real app: before, dozzle crash-looped on "Could not connect"; after, it reports "Connected to Docker", serves, and reads a neighbouring app's logs with sh.hola.app labels intact. The envelope still holds — stats, archive, images, secrets, volumes, POST restart, POST exec and DELETE all answer 403, and the neighbour's inspect carries no Env. Claude-Session: https://claude.ai/code/session_01Vck5KSX2CLxhohx14nb5Sh Co-authored-by: Paul O'Fallon <paulofallon@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Carries #468: the container-logs@1 grant can now serve a real Docker client. The contract has shipped since 0.11.0-rc.1 with no provider to exercise it. Packaging one (Dozzle, try-hola/apps#160) found three ways the proxy refused the clients it exists for — HEAD was rejected outright, so every Docker client failed on its first call; `/info` was refused, which an SDK client treats as "no engine"; and the redacted inspect omitted structural fields clients walk without nil-checking, segfaulting them. All three were shape, not policy: the grant discloses exactly what it did before, and stats, archive, images, secrets, volumes, restart, exec and delete were all re-verified at 403 on a VM after the fix. This is the release a container-logs provider needs. On rc.6 or earlier such an app installs and its proxy sidecar appears, but the app itself crash-loops. Claude-Session: https://claude.ai/code/session_01Vck5KSX2CLxhohx14nb5Sh Co-authored-by: Paul O'Fallon <paulofallon@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A pre-release version is never merged into main's package.json — the catalog builder reads src/*/ on main for the stable entry and discovers pre-releases from GHCR tags published off a PR. Dozzle has no stable predecessor, so it enters the catalog as 0.1.0 or not at all. Requires hola 0.11.0-rc.7 or newer: the container-logs@1 grant only serves a usable Docker client from #468 onward. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vck5KSX2CLxhohx14nb5Sh
What
A Dozzle package, and the catalog-vocabulary change it needs to exist.
container-logs@1has been implemented in Hola since 0.11.0-rc.1 — the redacting Docker façade, the provider grant, the install consent step, thesh.hola.*labels, the tests — and nothing has ever called it. This is the app that does.Why Dozzle
Because it has never heard of Hola. It reads
DOCKER_HOSTlike any Docker tool, so the entire integration is one line of manifest:and the server does the rest — injects a
hola-docker-proxysidecar running its own image, holds the socket read-only there, pointsDOCKER_HOSTat it on every service in the deployment, and answers onlyGETon the container list, logs, events, a redacted inspect and the probes. The compose file here is mostly comment explaining what it deliberately does not contain: the socket mount a log viewer normally needs, which the validator rejects anyway.That makes it the sharpest possible test of a provisioned contract, where "declared" and "actually working" are hardest to pull apart — unlike
backup@1, where a provider can declare the contract and never wire its hooks (#159).The catalog had to catch up first
schemas/manifest.schema.jsonandbin/validate-manifest.mjsknew onlyauth@1,backup@1andpush@1. The contract was undeclarable here even though the server has implemented it for three releases —provides: ["container-logs@1"]failed CI with "not a known capability contract". Both now carry it.While adding it,
acceptsmoved onto its ownacceptableContractRefenum:container-logs@1hasimplicitparticipation, so there is no acceptor side to opt into, and naming it inacceptsis now a CI error rather than something Hola drops with a warning at runtime.The packager README gained the two axes it never documented —
declaredvsimplicit,brokeredvsprovisioned— plus a table of all four contracts. That table was checked againstCONTRACTSinpackages/shared/src/contracts.tsrather than written from memory.What Dozzle can't do here, by design
POST /containers/{id}/…DOZZLE_ENABLE_ACTIONS=falsePOST /execDOZZLE_ENABLE_SHELL=falseGET /containers/{id}/statsGET /infoBoth writes are switched off explicitly rather than left to 403 in the UI. The last two are the first real finding about where that allowlist sits, and worth a decision once this has run on a VM: stats are benign resource metrics, while
/infoleaks host kernel/OS/registry detail and probably should stay blocked or be redacted like inspect is.Identity
auth.mode: forward-auth, so Authentik gates the door — and Dozzle's ownforward-proxyprovider is pointed at Authentik's identity headers (X-authentik-username/-email/-name; Dozzle's defaults are Authelia's), so the person who signed in is the person Dozzle shows rather than everyone sharing one anonymous session.The README is blunt about the blast radius: anyone who can open this app can read every app's logs, and logs contain more than their authors intend. That is what the install-time consent is for.
Verification so far
node bin/validate-manifest.mjs— all 18 manifests OK, including dozzle.validateComposeDocumentagainst the compose — no errors, no warnings.DOZZLE_*variable used was checked to exist upstream rather than assumed.ghcr.io/amir20/dozzle:v11.1.0@sha256:7c4fb7f8…(linux/amd64, arm, arm64).Not yet done: a real install. Next step is a disposable VM — install with
--grant container-logs@1, confirm the sidecar appears andDOCKER_HOSTis rewritten, confirm logs from other apps stream, and record exactly which calls the allowlist refuses. I'd rather land that evidence before this merges.🤖 Generated with Claude Code
https://claude.ai/code/session_01Vck5KSX2CLxhohx14nb5Sh