Binnacle in a Container
Русская версия: 07-containers.ru.md.
How to put Binnacle into each app container, beside the harness, and open its dashboard from the host's browser without opening it to your network. It needs Binnacle 0.7.1 or later: versions before 0.7.0 bind the container's own loopback, which a published port does not reach, and 0.7.0's binn init turns on an autostart that a container without systemd cannot run. A container upgraded from 0.7.0 in place, without a rebuild, turns it off with binn daemon autostart off.
Why inside each app container
Binnacle goes where the harness runs. Its daemon reads the harness's session files and the checkout from disk, and the harness starts binn mcp as a local process. A Binnacle in a container of its own would see neither: no session, no checkout, no binn to start.
So each app container gets its own Binnacle: its own daemon, home, token and dashboard, for the one project in it. Twenty containers are twenty daemons, each on a host port of its own. What one costs is under What one container costs.
With the dev container feature
Binnacle's feature installs @it-bbox/binnacle from npm, keeps Binnacle's home on a volume of this container's own, binds the daemon to every interface inside the container, accepts the host-side address, and starts the daemon with the container. A devcontainer.json adds it after Node 24, publishes the port and keeps the harness's home on a volume:
{
"features": {
"ghcr.io/devcontainers/features/node:1": { "version": "24" },
"ghcr.io/it-bbox/binnacle/binnacle:1": { "version": "0.7.1", "hostPort": "7717" }
},
"appPort": ["127.0.0.1:7717:7717"],
"containerEnv": { "CLAUDE_CONFIG_DIR": "/home/node/.claude" },
"mounts": ["source=claude-${devcontainerId},target=/home/node/.claude,type=volume"],
"postCreateCommand": "sudo chown node /home/node/.claude"
}| Option | Default | Meaning |
|---|---|---|
version | latest | The npm version of @it-bbox/binnacle to install. Pin it, so that a rebuild does not upgrade Binnacle behind your back. |
hostPort | 7717 | The host port the container's 7717 is published or forwarded on. The daemon accepts 127.0.0.1 and localhost at it. |
- The port.
appPortpublishes the container's 7717 on the host's loopback,127.0.0.1:<hostPort>:7717; never write it as a bare<hostPort>:7717(why). An editor that forwards ports can forward it instead,"forwardPorts": [7717]: it takes 7717 on the host when that port is free and another otherwise, andhostPortmust name the port it shows, which takes a rebuild when it changes. - The harness's home. Claude Code keeps its settings, its session files and Binnacle's plugin in
~/.claude, and its MCP servers, the onebinn initregisters included, in~/.claude.json. A volume holds a directory, not a file, soCLAUDE_CONFIG_DIRputs both in one directory, here/home/node/.claude, and the volume keeps them through a rebuild. Binnacle reads them there without being told. Docker creates a new volume's mount point as root when the image has no such directory, sopostCreateCommandgives it to the container's user, with thesudothemcr.microsoft.com/devcontainersimages have; an image withoutsudomakes the directory itself, as the Dockerfile below does./home/nodeandnodeare the Node images' user, so write your own. - The allowed hosts live in the feature's start command,
/usr/local/share/binnacle-start.sh: afterbinn daemon stop, start the daemon again with it, since a plainbinn daemon startwould not accept the host's address. - The feature writes nothing into your repository, publishes no port and mounts no harness home: the three are your
devcontainer.json's.
By hand
For an image built without features, or a docker compose service. The image installs Binnacle and sets what every container of it shares:
FROM node:24-bookworm-slim
RUN npm install -g @it-bbox/binnacle@0.7.1 \
&& mkdir -p /var/lib/binnacle /home/node/.claude \
&& chmod 1777 /var/lib/binnacle \
&& chown node:node /home/node/.claude
ENV BINNACLE_HOME=/var/lib/binnacle/home \
BINNACLE_HOST=0.0.0.0 \
CLAUDE_CONFIG_DIR=/home/node/.claude
USER nodeBINNACLE_HOST=0.0.0.0binds every interface inside the container, so that a published port reaches the daemon. Clients inside still reach it over the container's loopback.BINNACLE_HOMEis a directory on the volume mounted at/var/lib/binnacle, which keeps the registration, the token and the history through a rebuild. The two directories are made in the image so that a new volume starts out writable:/var/lib/binnacleopen to every user, as/tmpis, since a dev container may change the user's UID after the build, and the daemon creates its home inside, its user's alone;/home/node/.claudethe user's own, which that change follows.
Each container then gives four things: the port, published on the host's loopback; BINNACLE_ALLOWED_HOSTS, naming that host port; the two volumes; and the start command, run each time the container starts:
services:
app:
build: .
ports:
- "127.0.0.1:7717:7717"
environment:
BINNACLE_ALLOWED_HOSTS: 127.0.0.1:7717,localhost:7717
volumes:
- .:/workspace
- binnacle:/var/lib/binnacle
- claude:/home/node/.claude
command: sh -c 'binn daemon status >/dev/null 2>&1 || { rm -f "$$BINNACLE_HOME/runtime.json"; binn daemon start; }; exec sleep infinity'
volumes:
binnacle:
claude:The start command removes the runtime file a stopped container left, since the process it names died with the container, then starts the daemon; a daemon already answering is left running. Replace exec sleep infinity with your app's own command. A dev container built from this image gives the port, the variable and the volumes in appPort, containerEnv and mounts, and starts the daemon in its postStartCommand, without sleep, which would never return:
binn daemon status >/dev/null 2>&1 || { rm -f "$BINNACLE_HOME/runtime.json"; binn daemon start; }A container whose daemon does not start is developed as before, without Binnacle.
Publish on 127.0.0.1 only
Publish the port as 127.0.0.1:<port>:7717, never as a bare <port>:7717. A bare port listens on every interface of the host, and Docker writes its own firewall rules for it, ahead of ufw's: a ufw that denies the port does not stop your local network from reaching the dashboard. On the host's loopback, only your own machine reaches it.
The token still guards every operation, but /health and the dashboard's files need none, and nothing on your network should be calling them.
A port for each container
Every daemon listens on 7717 inside its own container. On the host, each container needs its own port: 7717 for the first, 7718 for the second, and so on.
| Container | appPort or ports | hostPort, or BINNACLE_ALLOWED_HOSTS |
|---|---|---|
| first | 127.0.0.1:7717:7717 | 7717, or 127.0.0.1:7717,localhost:7717 |
| second | 127.0.0.1:7718:7717 | 7718, or 127.0.0.1:7718,localhost:7718 |
The allowed Host must name the host-side port. The browser sends Host: 127.0.0.1:7718, and the daemon, listening on 7717, refuses any Host it is not told to accept: with a port published under one number and allowed under another, the dashboard answers with a refusal. binn daemon status lists the addresses the daemon accepts.
Set up the project and open the dashboard
Run binn init once per project, inside the container, in the checkout: it registers the project with the container's Binnacle and Binnacle's MCP server with the harness, both on their volumes, so a rebuild keeps them. Run on the host, it would register the project with the host's Binnacle, which sees none of the container's sessions. It does not offer autostart there: a container has no service manager, and the feature or the start command starts the daemon.
Then, inside the container:
binn dashboard --printIt prints the address the host opens, http://127.0.0.1:<hostPort>/ with the token in its fragment, which never reaches a server. Open it in the host's browser. Plain binn dashboard prints the same address without the token, and says it could not open a browser, since the container has none. After the first time, the plain address works as a bookmark in that browser.
The rest is as on a host: Installation checklist, and Security and resilience for what the daemon accepts.
What one container costs
One daemon's resident memory and its home's size, with one project per container, measured in epic 23's exit demonstration. The home grows with the sessions Binnacle imports, so this is its size before the first.
| Measure | Value |
|---|---|
| The daemon's resident memory | 82 MiB (VmRSS 84,380 kB) |
| The Binnacle home's size | 284 KiB |
| Measured on | 2026-10-30: Binnacle 0.7.1 and the feature 1.0.0 in mcr.microsoft.com/devcontainers/javascript-node:24, Docker Desktop 4.89.0 (Engine 29.7.2) on Ubuntu 24.04.5, one project registered by binn init and no harness session yet |