Starting the daemon unconditionally gave every container a process, a listening socket, and an identity in someone's tailnet that it never asked for. Most containers never use remote access, so the daemon is now opt-in. - The entrypoint consumes a leading `--tailscale` argument (or FUSION_TAILSCALE=1, with `--no-tailscale` to override it back off) and strips it from the argument list, so everything after it stays a normal Fusion CLI invocation. - Arguments are rotated through shift/append rather than string concatenation, so values containing spaces survive as single argv entries. - Replaces the FUSION_DISABLE_TAILSCALED opt-out, which is redundant now that the default is off. - Document the flag, the userspace-networking mode (no NET_ADMIN/tun caps), the one-time `tailscale up`, and the /home/node mount that persists that login. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.9 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, and4041vs reserved4040port 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).
Tailscale remote access
The image ships the tailscale CLI, but the tailscaled daemon does not run by default — most
containers never use remote access. Fusion's tunnel spawns a bare tailscale funnel <port>, which
talks to that daemon over a local socket, so without it the tunnel dies immediately with
failed to connect to local tailscaled and exit 1.
Start the daemon by passing --tailscale before the normal CLI arguments:
docker run -p 4040:4040 \
-v /path/to/project:/workspace \
-v fusion-home:/home/node \
fusion --tailscale dashboard --host 0.0.0.0
The flag is consumed by the entrypoint and stripped from the argument list, so everything after it
is an ordinary Fusion CLI invocation. FUSION_TAILSCALE=1 does the same thing for Compose files and
other env-driven setups; --no-tailscale overrides it back off.
The daemon runs in userspace networking mode, so it needs neither --cap-add NET_ADMIN nor
--device /dev/net/tun — the documented docker run above is complete. That mode is sufficient for
tailscale serve/funnel, which proxy to a local port rather than route packets.
It starts logged out. Authenticate the machine once:
docker exec -it <container> tailscale up
Open the printed URL to approve the node. Login state is written under /var/lib/tailscale, which
the image symlinks into /home/node/.tailscale — so mounting a volume at /home/node (as above)
persists the login across container recreates. Funnel additionally requires HTTPS certificates
enabled and the funnel node attribute granted in your tailnet's ACL policy.
If the daemon is missing, logged out, or stopped, the dashboard's remote-access card reports that directly rather than failing with an unexplained exit code.
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/workspaceproject 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
nodeuser. - The builder stage runs
pnpm buildwithNODE_OPTIONS=--max-old-space-size=6144. The dashboard'svite buildexceeds V8's default old-space on a stock Docker Desktop VM and aborts the image build withFATAL 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. gitmust be available in the container runtime. The mounted project volume must preserve.gitmetadata and repository history for worktree operations; Fusion initializes missing repositories during project registration.- The root
Dockerfileinstalls withpnpm install --frozen-lockfilebefore copying full source, so every current workspace package/plugin manifest selected bypnpm-workspace.yamlmust be covered by a builder-stageCOPYbefore 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.mjsexpands the current workspace entries and rejects missing or duplicate builder pre-install COPY sources. Run it withpnpm test:scripts -- scripts/__tests__/dockerfile-workspace-manifests.test.mjswhenever workspace membership or Docker manifest copies change.