Files
fusion/docs/docker.md
gsxdsm bb11e493f7 fix(auth): restore Codex login and promote outboard resize targets
Operator report from a containerized dashboard: OpenAI Codex login never opened
a browser window at all, and floating windows still needed the FN-8015 follow-up.

- pi's `AuthPrompt` is a discriminated union — text, secret, select, manual_code —
  and FusionAuthStorage.login's interaction shim flattened every variant into
  `onPrompt({message, placeholder})`, discarding `type` and a select's `options`.
  pi's Codex `login()` OPENS with `prompt({type:"select"})` (Browser vs Device
  code) before emitting any auth URL, so the dashboard answered the method picker
  with the promise that waits for a pasted code — input the UI never solicits,
  because nothing had been surfaced yet. The flow hung until the route's 30s
  kickoff timeout: "Login initiation timed out", no window. The route's
  onSelect/selectOauthOption has had the right answer since FN-5917, but the
  callback was dead code from the moment login moved to pi's ModelRuntime.
  Verified against a real container: the login endpoint now returns Codex's
  auth.openai.com URL in 0.03s instead of timing out after 30s.
- Promote FN-8766's outboard east/NE/SE resize targets from Task Detail to every
  desktop window. With FN-8015's body gutter deleted, a hosted scrollbar sits
  flush against the painted edge where those hit zones used to cover it (issue
  #2140); moving the targets outside the shell keeps it grabbable without
  insetting anything. That needs the host to stop clipping, so the body and its
  direct child inherit the corner radius — only 8 of ~30 callers set that
  themselves — and phones re-assert clipping since they hide every handle.
- Document the fixed OAuth callback ports (Anthropic 53692, Codex 1455) and
  PI_OAUTH_CALLBACK_HOST for Docker: without them the browser callback cannot
  reach the container's loopback listener, which is why subscription logins
  appeared to fail there.

Verified: 14989 dashboard tests, 58 engine auth-storage tests (4 new, covering
each prompt type), pnpm test:gate, eslint, and both typechecks all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 17:38:26 -07:00

6.2 KiB

Running Fusion in Docker

This guide shows how to build and run Fusion in a container.

This document is about containerizing Fusion itself (docker build / docker run). For managed Docker mesh-node provisioning architecture (services, routes, mesh config flow, and 4041 vs reserved 4040 port convention), see Architecture → Docker Node Provisioning.

Build the image

docker build -t fusion .

Run the dashboard

Mount your project into /workspace and publish the dashboard port:

docker run -p 4040:4040 -v /path/to/project:/workspace fusion

The application itself is installed under /app; /workspace is reserved for your project and is the container's working directory. Do not mount over /app.

By default, the container runs:

fn dashboard

on port 4040.

Environment variables

Pass provider credentials and integrations with -e flags:

-e ANTHROPIC_API_KEY=...
-e OPENAI_API_KEY=...
-e GITHUB_TOKEN=...
-e FUSION_DASHBOARD_TOKEN=fn_your_stable_token   # optional; persists across restarts

Add any other provider keys your setup requires (for example OPENROUTER_API_KEY).

Dashboard authentication

The dashboard is bearer-token protected by default. In a container the auto-generated token appears in docker logs on startup — copy it, or set FUSION_DASHBOARD_TOKEN (or the back-compat FUSION_DAEMON_TOKEN) to a stable value so the token survives restarts. See CLI reference → fn dashboard → Authentication for the full flow.

Provider OAuth logins (Anthropic, OpenAI Codex)

Subscription logins finish on a loopback callback server that the container runs itself, on fixed ports: 53692 for Anthropic and 1455 for OpenAI Codex. Two things make that unreachable by default — the port is not published, and the listener binds 127.0.0.1 inside the container, so publishing alone still would not deliver traffic arriving on the container's external interface. The symptom is a browser that lands on a connection-error page after you approve the login.

Publish both ports and bind the listener to all interfaces:

docker run -p 4040:4040 -p 53692:53692 -p 1455:1455 \
  -e PI_OAUTH_CALLBACK_HOST=0.0.0.0 \
  -v /path/to/project:/workspace \
  -v fusion-home:/home/node/.fusion \
  fusion

The browser callback then completes on its own, with nothing to paste. Both ports are fixed by the provider's registered redirect URI, so they cannot be remapped to different host ports — -p 53692:53693 will not work.

Without this, the fallback is manual: copy the full URL from the browser's address bar after approving and paste it into the login card. Note the callback listener accepts connections from outside the container while a login is in flight; it is short-lived and validates the OAuth state, but prefer publishing these ports only on a trusted network (-p 127.0.0.1:53692:53692 restricts them to the host).

Pass additional CLI flags

You can append normal CLI arguments after the image name:

docker run fusion dashboard --port 8080

If you change the dashboard port, also update Docker port mapping:

docker run -p 8080:8080 fusion dashboard --port 8080

Persistence

Fusion keeps state in two places inside the container:

  • Per-project state — .fusion/ under the mounted project (/workspace/.fusion). This is covered automatically by the /workspace project mount.
  • Global state — /home/node/.fusion (embedded PostgreSQL data, global settings, agents). This is not under /workspace, so mount it separately if you want it to survive container removal:
docker run -p 4040:4040 \
  -v /path/to/project:/workspace \
  -v fusion-home:/home/node/.fusion \
  fusion

The named volume fusion-home persists the embedded database across docker run invocations; a host directory bind mount works too.

The image pre-creates /home/node/.fusion owned by node, so a fresh named volume inherits that ownership and embedded PostgreSQL can initialize on first run. A bind mount does not inherit it — the host directory's ownership wins — so a host path mounted there must already be writable by uid 1000:

mkdir -p /path/to/fusion-home && sudo chown -R 1000:1000 /path/to/fusion-home

Symptom when this is wrong: initdb: error: could not create directory "/home/node/.fusion/embedded-postgres": Permission denied, followed by the dashboard supervisor exhausting its restarts and the container reporting unhealthy.

Complete example

docker run --rm \
  -p 4040:4040 \
  -v /path/to/project:/workspace \
  -v fusion-home:/home/node/.fusion \
  -e ANTHROPIC_API_KEY=your_key \
  -e OPENAI_API_KEY=your_key \
  -e GITHUB_TOKEN=your_token \
  fusion dashboard --port 4040

Notes

  • The container runs as the non-root node user.
  • The builder stage runs pnpm build with NODE_OPTIONS=--max-old-space-size=6144. The dashboard's vite build exceeds V8's default old-space on a stock Docker Desktop VM and aborts the image build with FATAL ERROR: Ineffective mark-compacts near heap limit (exit 134). The value is a ceiling, not a reservation. If your Docker VM has less than ~8GB, raise its memory allocation rather than lowering this number.
  • git must be available in the container runtime. The mounted project volume must preserve .git metadata and repository history for worktree operations; Fusion initializes missing repositories during project registration.
  • The root Dockerfile installs with pnpm install --frozen-lockfile before copying full source, so every current workspace package/plugin manifest selected by pnpm-workspace.yaml must be covered by a builder-stage COPY before that install. Keep the manifest-only dependency-cache layer; the runner's intentionally filtered production install does not provide builder coverage.
  • scripts/__tests__/dockerfile-workspace-manifests.test.mjs expands the current workspace entries and rejects missing or duplicate builder pre-install COPY sources. Run it with pnpm test:scripts -- scripts/__tests__/dockerfile-workspace-manifests.test.mjs whenever workspace membership or Docker manifest copies change.