# Federation testing in Docker
Reproduce a GitHub-Actions-like environment locally: two maw-js
containers on a shared Docker network, handshaking as peers.
## Topology
```mermaid
flowchart LR
host(["Host machine"])
host -->|localhost:13456| A_port
host -->|localhost:13457| B_port
subgraph bridge["Docker bridge network"]
direction LR
subgraph A["node-a (maw-node-a)"]
A_port["port 3456"]
A_vol[("maw-a-data
→ /root/.maw")]
A_hc{{"healthcheck:
/api/plugins"}}
end
subgraph B["node-b (maw-node-b)"]
B_port["port 3456"]
B_vol[("maw-b-data
→ /root/.maw")]
B_hc{{"healthcheck:
/api/plugins"}}
end
A -->|"probe → http://node-b:3456/info"| B
B -->|"probe → http://node-a:3456/info"| A
end
```
Host ports `13456` / `13457` map to each container's `3456`. Inside the
bridge network, containers reach each other by hostname (`node-a`,
`node-b`) via Docker's embedded DNS — no `localhost` shortcut, so peer
handshake bugs surface the same way they would across real hosts.
## Why this exists
Peer handshake bugs only surface across real network boundaries — on a
single host everything talks via `localhost` and subtle URL / DNS /
CORS issues hide. Running two containers behind Docker's internal DNS
gives us a cheap, reproducible 2-node cluster that mirrors what CI and
production peer pairs actually see.
## Run it locally
```bash
# One-shot: build + up + probe + teardown
bash scripts/test-docker-federation.sh
# Or step-through (leaves containers running):
bash scripts/dev-federation.sh up
docker compose -f docker/compose.yml exec node-a maw peers probe peer
docker compose -f docker/compose.yml exec node-b maw peers probe peer
bash scripts/dev-federation.sh down
```
Requires Docker Engine 24+ and `docker compose` v2.
## Expected output
```
--- probe a → b ---
probing peer → http://node-b:3456 ...
✓ reached peer (node-b)
--- probe b → a ---
probing peer → http://node-a:3456 ...
✓ reached peer (node-a)
## Docker federation probe result
- a → b: PASS, code: 0, hint: -
- b → a: PASS, code: 0, hint: -
OK: both directions passed
```
The script exits `0` only if both probe calls exit `0` **and** the
output contains no `handshake failed` substring. Any other shape is a
regression.
Last verified end-to-end on 2026-04-19 against `main` at
[`76e1db1`](https://github.com/Soul-Brews-Studio/maw-js/commit/76e1db1) —
both directions PASS once the bind heuristic from
[#619](https://github.com/Soul-Brews-Studio/maw-js/pull/619) (closes
[#616](https://github.com/Soul-Brews-Studio/maw-js/issues/616)) is in
place.
## Debugging failures
1. Re-run with the stack left up: `bash scripts/dev-federation.sh up`
2. Shell into a node: `docker compose -f docker/compose.yml exec node-a sh`
3. Inspect logs: `docker compose -f docker/compose.yml logs node-a node-b`
4. Check healthchecks: `docker compose -f docker/compose.yml ps`
5. Manual probe inside a node: `maw peers probe peer`
On CI, the `Federation (Docker) integration` workflow uploads compose
logs as a `federation-docker-logs` artifact when the job fails.
## Known gaps
- **Historical (resolved, #596 / #603):** `maw-js` did not register a
`/info` endpoint, so `src/commands/plugins/peers/probe.ts` surfaced
`HTTP_4XX` against any currently-built image. The probe round-trip was
still useful for catching transport / DNS / compose-wiring regressions,
but the handshake classifier stayed red until `/info` shipped. Tracking
issue: [#596](https://github.com/Soul-Brews-Studio/maw-js/issues/596)
(closed by [#603](https://github.com/Soul-Brews-Studio/maw-js/pull/603)).
- **Historical (resolved, #607 / #614):** the image build failed at
`bun install --frozen-lockfile` when the committed `bun.lock` was
written by a bun version older than the one resolved by
`oven/bun:1.3-alpine`. Aligning the Dockerfile to the lockfile's bun
version unblocked the harness.
- **Historical (resolved, #616 / #619):** `maw serve` defaulted to
binding `127.0.0.1` only, so cross-container probes resolved DNS but
hit `REFUSED` on connect. The bind heuristic now upgrades to `0.0.0.0`
when `MAW_HOST` is set or when `peers.json` exists, which is what the
compose entrypoint provides.
## Related
- `docker/Dockerfile` — the `maw-js:test` image (single-stage, bun-alpine)
- `docker/compose.yml` — 2-node wiring with mutual `PEER_URL`s
- `scripts/dev-federation.sh` — local up/down helper
- `scripts/test-docker-federation.sh` — end-to-end probe driver
- `.github/workflows/federation-docker.yml` — CI wrapper