forked from Runfusion/Fusion
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile
More file actions
249 lines (224 loc) · 17.8 KB
/
Copy pathDockerfile
File metadata and controls
249 lines (224 loc) · 17.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# syntax=docker/dockerfile:1
FROM node:22-slim AS builder
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends git build-essential python3 \
&& rm -rf /var/lib/apt/lists/*
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
# FNXC:DockerBuild 2026-08-10-18:03: This list is derived from pnpm-workspace.yaml.
# Every selected workspace manifest must be copied before frozen install, or pnpm
# omits its dependencies and the later full-workspace image build can fail.
COPY packages/cli/package.json ./packages/cli/package.json
COPY packages/cli-alias/package.json ./packages/cli-alias/package.json
COPY packages/core/package.json ./packages/core/package.json
COPY packages/dashboard/package.json ./packages/dashboard/package.json
COPY packages/desktop/package.json ./packages/desktop/package.json
COPY packages/droid-cli/package.json ./packages/droid-cli/package.json
COPY packages/engine/package.json ./packages/engine/package.json
COPY packages/i18n/package.json ./packages/i18n/package.json
COPY packages/mobile/package.json ./packages/mobile/package.json
COPY packages/pi-claude-cli/package.json ./packages/pi-claude-cli/package.json
COPY packages/pi-llama-cpp/package.json ./packages/pi-llama-cpp/package.json
COPY packages/plugin-sdk/package.json ./packages/plugin-sdk/package.json
COPY plugins/examples/fusion-plugin-auto-label/package.json ./plugins/examples/fusion-plugin-auto-label/package.json
COPY plugins/examples/fusion-plugin-ci-status/package.json ./plugins/examples/fusion-plugin-ci-status/package.json
COPY plugins/examples/fusion-plugin-notification/package.json ./plugins/examples/fusion-plugin-notification/package.json
COPY plugins/examples/fusion-plugin-settings-demo/package.json ./plugins/examples/fusion-plugin-settings-demo/package.json
COPY plugins/fusion-plugin-acp-runtime/package.json ./plugins/fusion-plugin-acp-runtime/package.json
COPY plugins/fusion-plugin-todos/package.json ./plugins/fusion-plugin-todos/package.json
COPY plugins/fusion-plugin-compound-engineering/package.json ./plugins/fusion-plugin-compound-engineering/package.json
COPY plugins/fusion-plugin-linear-import/package.json ./plugins/fusion-plugin-linear-import/package.json
COPY plugins/fusion-plugin-paperclip-runtime/package.json ./plugins/fusion-plugin-paperclip-runtime/package.json
COPY plugins/fusion-plugin-dependency-graph/package.json ./plugins/fusion-plugin-dependency-graph/package.json
COPY plugins/fusion-plugin-cli-printing-press/package.json ./plugins/fusion-plugin-cli-printing-press/package.json
COPY plugins/fusion-plugin-openclaw-runtime/package.json ./plugins/fusion-plugin-openclaw-runtime/package.json
COPY plugins/fusion-plugin-hermes-runtime/package.json ./plugins/fusion-plugin-hermes-runtime/package.json
COPY plugins/fusion-plugin-droid-runtime/package.json ./plugins/fusion-plugin-droid-runtime/package.json
COPY plugins/fusion-plugin-cursor-runtime/package.json ./plugins/fusion-plugin-cursor-runtime/package.json
COPY plugins/fusion-plugin-grok-runtime/package.json ./plugins/fusion-plugin-grok-runtime/package.json
COPY plugins/fusion-plugin-claude-runtime/package.json ./plugins/fusion-plugin-claude-runtime/package.json
COPY plugins/fusion-plugin-omp-runtime/package.json ./plugins/fusion-plugin-omp-runtime/package.json
COPY plugins/fusion-plugin-quality/package.json ./plugins/fusion-plugin-quality/package.json
COPY plugins/fusion-plugin-agent-browser/package.json ./plugins/fusion-plugin-agent-browser/package.json
COPY plugins/fusion-plugin-whatsapp-chat/package.json ./plugins/fusion-plugin-whatsapp-chat/package.json
COPY plugins/fusion-plugin-roadmap/package.json ./plugins/fusion-plugin-roadmap/package.json
COPY plugins/fusion-plugin-even-realities-glasses/package.json ./plugins/fusion-plugin-even-realities-glasses/package.json
COPY plugins/fusion-plugin-reports/package.json ./plugins/fusion-plugin-reports/package.json
RUN pnpm install --frozen-lockfile
COPY . .
# FNXC:DockerBuild 2026-08-17-23:18: The dashboard's `vite build` transforms ~5.7k modules and
# exceeded V8's default old-space on a stock Docker Desktop VM (8GB), aborting the whole image
# build with "FATAL ERROR: Ineffective mark-compacts near heap limit" (exit 134). The ceiling is
# a cap, not a reservation — V8 only grows to what the build needs — so raising it here costs
# nothing on larger hosts and is the difference between a working and a failing `docker build`
# on a default install. Scoped to this RUN so it never leaks into the runner stage's env.
RUN NODE_OPTIONS=--max-old-space-size=6144 pnpm build
FROM node:22-slim AS runner
LABEL org.opencontainers.image.source="https://github.com/gsxdsm/fusion"
LABEL org.opencontainers.image.description="AI-orchestrated task board"
ENV NODE_ENV=production
ENV PORT=4040
# FNXC:DockerRun 2026-08-18-05:35: ca-certificates is REQUIRED, not optional hardening. The slim
# base ships zero CA certificates, and git verifies TLS against the SYSTEM store — so every HTTPS
# clone failed with "server certificate verification failed. CAfile: none CRLfile: none", which
# breaks project setup outright (operator report). It hid behind Node, which carries its own bundled
# CA store: the dashboard, model APIs, and OAuth token exchanges all worked, so the image looked
# healthy right up until the first clone.
# FNXC:DockerRun 2026-08-18-06:05: ripgrep ships by default because the coding agents Fusion drives
# reach for `rg` as their primary search tool; without it they silently degrade to slower/partial
# fallbacks inside the container while working fine on a developer machine that has it installed.
# FNXC:DockerRun 2026-08-20-04:30: git-lfs ships by default because this repository stores binary
# assets (screenshots) as LFS objects. Without it, git silently checks out 130-byte POINTER FILES
# instead of the real content and reports a clean tree — so an agent reads a text stub where an image
# should be, and `git lfs` subcommands in any workflow fail outright. It is a git dependency, not an
# optional extra: the failure is silent corruption of a working checkout, not a missing feature.
RUN apt-get update \
&& apt-get install -y --no-install-recommends git git-lfs ca-certificates ripgrep curl gnupg \
&& rm -rf /var/lib/apt/lists/*
# FNXC:DockerRun 2026-08-18-06:40: gh, tailscale, and cloudflared ship in the image.
# Rationale per tool: `gh` backs Fusion's GitHub integration (githubAuthMode "gh-cli" is a documented
# option and the auth route tells operators to run `gh auth login`, which is impossible if the binary
# is absent); `cloudflared` backs the dashboard's remote-access feature, whose installer cannot
# bootstrap itself reliably inside a slim container; `tailscale` gives the same box a private-network
# option. All three come from their vendors' own apt repositories with signed keyrings rather than
# curl-to-shell installers, so upgrades and signature checks follow the normal apt path.
#
# NOTE: installing tailscale does NOT make `tailscaled` runnable by itself — the daemon additionally
# needs `--cap-add NET_ADMIN --device /dev/net/tun` on `docker run`. Shipping the binary is the part
# the image can own; granting kernel capabilities stays an explicit operator decision.
#
# FNXC:DockerRun 2026-08-27-20:00: google-chrome-stable joins them, because the image shipped NO
# browser at all and two shipped features silently need one. `plugins/fusion-plugin-agent-browser`
# drives a browser through `playwright-core`, which by design does NOT download one at install time
# (`src/driver.ts`), and probes fixed paths whose first two Linux entries are `/usr/bin/google-chrome`
# and `/usr/bin/google-chrome-stable` (`src/probe.ts`). The Chrome DevTools MCP server
# (`chrome-devtools-mcp`) likewise launches an EXISTING Chrome and downloads nothing; with no browser
# present it fails every call with "Could not find Google Chrome executable for channel 'stable'".
# Google Chrome stable is chosen over Debian's `chromium` because it is the only browser
# chrome-devtools-mcp officially supports (its README supports Chrome and Chrome for Testing only,
# and treats other Chromium builds as unsupported), and Google publishes it for BOTH amd64 and arm64,
# so `arch=$(dpkg --print-architecture)` resolves on either host. Measured 2026-08-27 in the stable
# repo index: google-chrome-stable 152.0.7977.64-1 for amd64 and arm64.
#
# NOTE: shipping the binary does not make Chrome's own sandbox usable. Chrome sandboxes itself with
# unprivileged user namespaces, which the default container seccomp profile blocks — `unshare -U`
# returns EPERM here even though the kernel allows namespaces (max_user_namespaces=126143), and
# Chrome then aborts at startup with "No usable sandbox!". Callers must therefore either pass
# `--no-sandbox` (chrome-devtools-mcp: `--chromeArg=--no-sandbox`) or the operator must relax the
# profile at `docker run` with `--security-opt seccomp=unconfined`. Same division of responsibility
# as tailscale above: the image owns the binary, kernel privileges stay an operator decision.
#
# External integration evidence:
# gh — repo https://github.com/cli/cli, docs https://cli.github.com/,
# apt https://cli.github.com/packages, binary `gh`, key
# githubcli-archive-keyring.gpg (vendor-signed; upstream-pending-verification)
# tailscale — repo https://github.com/tailscale/tailscale, docs https://tailscale.com/download/linux,
# apt https://pkgs.tailscale.com/stable/debian, binaries `tailscale`/`tailscaled`,
# key bookworm.noarmor.gpg (vendor-signed; upstream-pending-verification)
# cloudflared — repo https://github.com/cloudflare/cloudflared, docs https://pkg.cloudflare.com/,
# apt https://pkg.cloudflare.com/cloudflared, binary `cloudflared`,
# key cloudflare-main.gpg (vendor-signed; upstream-pending-verification)
# chrome — repo https://chromium.googlesource.com/chromium/src/ (upstream Chromium; Chrome
# itself is closed-source and has no public repo), docs https://www.google.com/chrome/,
# apt https://dl.google.com/linux/chrome/deb/ stable main, binaries
# `google-chrome-stable`/`google-chrome`, key
# https://dl.google.com/linux/linux_signing_key.pub (ASCII-armored, so it is
# gpg --dearmor'ed into the keyring; vendor-signed; upstream-pending-verification)
# Consumer docs: https://github.com/ChromeDevTools/chrome-devtools-mcp
RUN install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list \
&& curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.noarmor.gpg -o /usr/share/keyrings/tailscale-archive-keyring.gpg \
&& curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.tailscale-keyring.list -o /etc/apt/sources.list.d/tailscale.list \
&& curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg -o /usr/share/keyrings/cloudflare-main.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main" > /etc/apt/sources.list.d/cloudflared.list \
&& curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/google-chrome.gpg] https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh tailscale cloudflared google-chrome-stable \
&& rm -rf /var/lib/apt/lists/*
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
# FNXC:DockerRun 2026-07-23-00:00: The app install root must be distinct from the
# documented user-project mount point. Installing the app at /project made the
# documented `-v host:/project` bind mount shadow the CLI (MODULE_NOT_FOUND on
# packages/cli/dist/bin.js). The app now lives at /app; users mount their project
# at /workspace, which is also the runtime working directory (issue #2414).
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/cli/package.json ./packages/cli/package.json
COPY packages/core/package.json ./packages/core/package.json
COPY packages/dashboard/package.json ./packages/dashboard/package.json
COPY packages/engine/package.json ./packages/engine/package.json
RUN pnpm install --frozen-lockfile --prod \
--filter @runfusion/fusion
COPY --from=builder /app/packages/core/dist ./packages/core/dist
COPY --from=builder /app/packages/engine/dist ./packages/engine/dist
COPY --from=builder /app/packages/dashboard/dist ./packages/dashboard/dist
COPY --from=builder /app/packages/cli/dist ./packages/cli/dist
# @runfusion/fusion references typebox at runtime via the bundled CLI.
COPY --from=builder /app/node_modules/.pnpm/typebox@*/node_modules/typebox /app/node_modules/typebox
# FNXC:DockerRun 2026-07-23-00:00: /workspace is the documented bind-mount point for
# the user's project and the container working directory, so `fn dashboard` operates
# on the mounted project. It must stay empty in the image so a bind mount never
# shadows application code.
# FNXC:DockerRun 2026-08-17-23:18: /home/node/.fusion must exist node-owned IN THE IMAGE, because
# Docker seeds a fresh NAMED volume from the image's content and ownership at the mount path. The
# documented `-v fusion-home:/home/node/.fusion` invocation previously mounted a root-owned empty
# volume over a path that did not exist, so embedded Postgres `initdb` failed with "could not create
# directory ... Permission denied", the dashboard supervisor burned its 4 restarts, and the container
# went unhealthy on first run. Pre-creating it makes the documented command work with no host-side
# chown. NOTE: this fixes named volumes only — a BIND mount keeps the host directory's ownership, so
# a host path bound here must already be writable by uid 1000 (node).
RUN chown node:node /app \
&& mkdir -p /workspace /home/node/.fusion \
&& chown node:node /workspace /home/node/.fusion
# FNXC:DockerRun 2026-08-23-02:03: tailscaled runs as `node`, not root, so its default socket and
# state directories must exist node-owned BEFORE the USER switch — the daemon cannot mkdir them under
# root-owned /var/run and /var/lib itself. /var/lib/tailscale is a SYMLINK into /home/node/.tailscale
# rather than a real directory: the documented `-v <vol>:/home/node` mount then carries the node's
# login state, so an authenticated container survives `docker rm` + recreate instead of demanding a
# fresh `tailscale up` every rebuild. /var/log/tailscaled.log is pre-created for the same
# ownership reason.
RUN mkdir -p /var/run/tailscale /home/node/.tailscale \
&& rm -rf /var/lib/tailscale \
&& ln -sfn /home/node/.tailscale /var/lib/tailscale \
&& touch /var/log/tailscaled.log \
&& chown node:node /var/run/tailscale /home/node/.tailscale /var/log/tailscaled.log
COPY --chmod=0755 scripts/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
USER node
# FNXC:DockerRun 2026-08-18-06:55: A DEFAULT GIT IDENTITY, because a container has none and Fusion
# mostly commits with whatever git finds in ambient config. Only `workspace-fence-ref.ts` passes
# `-c user.name/-c user.email` explicitly; the merge commits, the `--amend` in merger-ai, and the
# experiment git-ops all rely on the environment. With no identity every one of them dies on
# "Author identity unknown ... Please tell me who you are", so an auto-merge reached `status:merging`
# and stopped there with nothing in the UI to explain why (operator report).
#
# The values match the identity Fusion already uses for its own fence commits, so authorship stays
# consistent; an operator who wants real authorship overrides it with `git config --global` in a
# mounted home or a derived image. This is a FALLBACK for the container, not a substitute for
# passing an explicit identity at the commit sites — those should still be fixed upstream so a bare
# machine with no git config behaves the same way.
RUN git config --global user.name "Fusion" \
&& git config --global user.email "fusion@localhost" \
&& git config --global init.defaultBranch main
WORKDIR /workspace
EXPOSE 4040
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD node -e "fetch('http://localhost:4040/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
# FNXC:DockerRun 2026-07-23-00:00: Entrypoint uses the absolute app path so it works
# regardless of the working directory or any volume mounted at /workspace.
# FNXC:DockerRun 2026-08-23-02:03: The wrapper script consumes its own opt-in `--tailscale` flag and
# then runs that same absolute-path node invocation with the REMAINING args verbatim, so every
# documented `docker run ... dashboard --host 0.0.0.0` argument list behaves exactly as before.
# FNXC:DockerSourceUpdate 2026-09-01-01:22: PID 1 is now the wrapper acting as the RESTART SUPERVISOR
# rather than the dashboard itself. Without a supervising parent the dashboard reported
# restartSupported=false, so the System panel's Restart button — and therefore the whole
# dashboard-only "pull, rebuild, restart" path a remote contributor depends on — was dead in the
# container. The wrapper forwards SIGTERM/SIGINT/SIGHUP to the child and propagates its exit code, so
# `docker stop` still shuts down gracefully and a crash still ends the container with its real status.
# `--from-source` (or FUSION_FROM_SOURCE=1) runs the CLI from the FUSION_SOURCE_ROOT git checkout
# instead of the image-baked /app, which is what makes an in-container source update possible.
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["dashboard", "--host", "0.0.0.0"]