ADR 0162 — The broker becomes the fifth published image, and the installer ASKS whether to turn it on
Status
Accepted. Decided by the maintainer on 2026-09-18 (AT-097). It closes decision 7 of ADR 0150 ("the broker does not come up in this phase, and the consequence is stated"), extends ADR 0119 from four to five images and applies to the installation the argument that ADR 0146 (RN-512) applied to the development compose. The broker itself — the five layers, the five operations, the computed spec — is that of ADR 0130, and nothing in it changes. None of these is edited; this document references them.
Sibling: ADR 0161
delivered the WEB half of the same maintainer decision ("both") — project
creation only offers the mode the installation executes — and declared in writing
that the INSTALLER half stopped because the image was not published. This ADR is
that half. The two complete each other without depending on each other: that one
prevents the screen from offering what the installation does not execute; this
one gives the installation a way to execute, with consent. With the broker on,
the installation gets BROKER_URL, and it is through it that 0161's screen
offers both modes again.
Context
In a real installation, a container or mounted project never executes.
Whoever brings up the container of those two modes is the broker
(ADR 0144 branches by DESTINATION:
container and mounted → broker), and the installation compose does not have
the service. BROKER_URL stays undefined, container_start ends in
container.start_failed with BrokerIndisponivelError, and the dev agents stay
in dev.blocked_by_container forever — RN-502 refuses, rightly, to execute
outside a running container.
Decision 7 of ADR 0150 declared this and called the fix small. Measured on 2026-09-18, when opening this front:
docker manifest inspect ghcr.io/daneiel/brabo-broker:6.1.0→denied; the same question forbrabo-api:6.1.0resolves. The image does not exist in the registry.docker-bake.hclhad four targets (api,engine,web,backup), andscripts/ci/images-manifest.tsaccepted exactly the four.docker/broker/Dockerfile.prodalready existed, and already went throughci.yml'shadolinttogether with the other four —FROMby digest with the tag on the line above,USER node,HEALTHCHECKagainst/health, npm/corepack removed. What was missing was not the image: it was someone building, scanning and publishing it.ci.ymlnever built it;docker-compose.prod.ymlreferenced it asbrabo-broker:prod, which only existed on the machine of whoever randocker compose buildwith the profile.- The Release does not bring the code, so
--source=local— the path ADR 0150 pointed to as the way out — requires a checkout on a tag, which is not the one-line installation.
Decision
1. The broker is the FIFTH published image, with the same gates as the others
A broker target in docker-bake.hcl, inside the default group: ci.yml now
builds it on every PR, checks that it does not run as root, scans it in Trivy
with the same rules (HIGH,CRITICAL, --ignore-unfixed) and proves it COMES UP
healthy with a read-only rootfs. release.yml publishes it by DIGEST,
images-manifest.ts requires it (a missing target still fails — five of five or
none), and keyless cosign signing and the verification in the same run
(RN-524) cover it without a new line, because both
loops read the manifest.
Kubernetes does not know it, and that is a decision, not forgetfulness: there
is no broker Deployment in deploy/k8s/, and mounting a node's socket into a pod
is another privilege conversation. argumentosDeSetImage now emits only the
images the kustomize base declares — make imagens-do-release keeps writing four
lines into the overlay.
2. The installation compose gains the service, OFF by default
The service enters under profiles: ["container-broker"], the SAME profile as
the validation compose, and with the five layers of ADR 0130 intact: no
ports:, only on the broker network (internal: true, which only the api
reaches and which has no egress), BRABO_SERVICE_TOKEN mandatory in production,
five operations, a spec computed from what the api says. The socket is mounted in
it and in no other. api now always participates in the broker network — as it
already does in the validation compose —, and BROKER_URL in the api stays empty
unless the .env says otherwise.
The image comes from BRABO_BROKER_IMAGE, mandatory like the other four, and
that was measured and not chosen: Compose interpolates the WHOLE file before
filtering by profile, so a ${VAR:?…} in a disabled service refuses the file all
the same. The installer always writes it (it comes from the same manifest),
whether it turns on the broker or not.
The two roots go as in the validation compose: BRABO_PROJECTS_HOST_BASE derives
from BRABO_PROJECTS_BASE, which is already a host path. The other,
PROJECT_WORKSPACES_HOST_ROOT, has no way to derive in the compose — the
installation's managed folder is the NAMED volume project_workspaces, and the
path the daemon resolves is the HOST's. The installer COMPUTES it
(<DockerRootDir>/volumes/brabo_project_workspaces/_data, the local driver's
layout) and, after startup, CHECKS it against the Mountpoint the daemon returns
for the volume; diverging becomes a named pending item with the right value,
never a silently guessed value.
3. The installer ASKS, with the risk stated in text, and the default is NO
A new question, after the base and before the .env. It says what turning it on
grants — a service of this installation starts talking to this machine's Docker,
and whoever commands that service commands Docker —, what the layers contain, and
what changes without it (container and mounted projects do not execute; the
Runner mode does not depend on it). Only s/sim turns it on. Enter, anything
else or the lack of a terminal leave it off, and the script SAYS it stayed off.
Yes writes into the .env COMPOSE_PROFILES=container-broker,
BROKER_URL=http://broker:8090, the DOCKER_GID and the computed root. The
DOCKER_GID is measured from inside a container, with the broker image
itself and the socket mounted through --mount type=bind (which refuses instead
of creating the folder when the source does not exist), because it is the gid the
process sees there that group_add needs — and on Docker Desktop the socket's gid
on the host says nothing about the one inside the VM. Not being able to measure,
or the path not being a socket, is a NAMED refusal that teaches to run again and
answer no; never a guessed 999.
The decision appears in the final summary, and the sentence "what this installer
does NOT do" stops saying that only "Mounted folder" stays without a container:
container does too.
4. Why RN-512's argument holds here
ADR 0146 separated the two composes by what the socket MEANS: in development whoever brings up the stack already has the socket, so the broker receiving it grants nothing the operator does not have; in production the socket is a privilege boundary and the operator is not the developer.
The one-line installation is the first case, not the second. It runs on the
person's own machine, who just ran docker compose up — the installer does not
work without them having the socket. Operator and user are the same person. What
the installation CANNOT assume, and that is why it asks instead of turning it on
like the development compose, is that whoever installs knows what the socket is:
the dev compose is read by whoever develops the product; the installer is run by
whoever never opened this repository. Explicit consent is the difference that
remains, and it is the one this ADR buys.
docker-compose.prod.yml does not change: it remains under a profile, and for it
ADR 0146's reading still holds without caveat.
Consequences
The price, declared.
- A root-equivalent image becomes public. Anyone can pull
ghcr.io/daneiel/brabo-broker. The image itself grants nothing — it is the mounted socket that grants —, but it becomes a studiable target, and a vulnerability in it becomes, in every installation that turned it on, a path to the host's Docker. That is why it enters each PR's Trivy and each tag's signature, and not a separate list. - The Docker socket is now mounted in a service of a user installation, when
it consents. Whoever commands the broker commands the machine's Docker. What
separates it from the rest: it publishes no port, only the api reaches it, the
token is compared in constant time, and there is no field where one could write
privileged,cap_add,network: hostor a free-v. That contains the caller; it does not contain a bug in the broker itself. BRABO_BROKER_IMAGEbecomes mandatory in the installation's.env, on or not. A.envwritten by a previous installer does not have it — but the installer rewrites the.envon every execution, and the update goes through it.- The installer requires
brokerinimages.json. A Release prior to this one does not have it, and this version's installer refuses naming the target. It is the same rule as ADR 0160: a tag's installer installs that tag. - The managed root depends on the
localdriver's layout. Computed before startup, checked after; what does not check out becomes a pending item with the value read from the daemon. - Not measured: the
DOCKER_GIDand the computed root on macOS Docker Desktop. The tag's E2E runs the interactive half only on Linux.
What does NOT change. The validation compose, smoke.sh (which comes up
without the profile and keeps proving the same three app images), the
development compose, Kubernetes, and the broker itself.