feat(docker): ship gh, tailscale and cloudflared in the image

Operator asked for cloudflared, tailscale, rg, git and gh available by default
in the container. git/ca-certificates/ripgrep already landed; this adds the
remaining three.

Each comes from its vendor's own signed apt repository rather than a
curl-to-shell installer, so signature checking and upgrades follow the normal
apt path:
  gh          https://cli.github.com/packages
  tailscale   https://pkgs.tailscale.com/stable/debian
  cloudflared https://pkg.cloudflare.com/cloudflared

Why each belongs in the image: gh backs Fusion's gh-cli GitHub auth mode (the
auth route instructs operators to run `gh auth login`, impossible without the
binary), cloudflared backs the dashboard's remote-access feature whose in-app
installer cannot bootstrap itself reliably in a slim container, and tailscale is
the private-network option for the same box.

Installing tailscale does NOT make tailscaled runnable by itself: the daemon
also needs --cap-add NET_ADMIN --device /dev/net/tun at docker run. Shipping the
binary is the image's part; granting kernel capabilities stays an explicit
operator decision.

Commands were validated live in a running container before being written here;
the guard test asserts both the repo wiring and the package names so half a
change cannot silently ship.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-08-17 22:48:49 -07:00
parent aedee4b823
commit 189087adf8
3 changed files with 64 additions and 1 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: The Docker image now ships gh, tailscale, and cloudflared alongside git and ripgrep.
category: feature
dev: Runner stage adds the GitHub CLI (backs `githubAuthMode: "gh-cli"`, which the auth route tells operators to set up with `gh auth login`), cloudflared (backs dashboard remote access, whose in-app installer cannot bootstrap itself reliably in a slim container), and tailscale, each from its vendor's signed apt repository rather than a curl-to-shell installer. Installing tailscale does not make `tailscaled` runnable on its own — that still needs `--cap-add NET_ADMIN --device /dev/net/tun` at `docker run`. Package names and repo URLs are asserted in scripts/__tests__/dockerfile-workspace-manifests.test.mjs.

View File

@@ -78,7 +78,41 @@ ENV PORT=4040
# 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.
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates ripgrep \
&& apt-get install -y --no-install-recommends git 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.
#
# 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)
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 \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh tailscale cloudflared \
&& rm -rf /var/lib/apt/lists/*
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate

View File

@@ -132,3 +132,25 @@ test("runner stage installs ca-certificates alongside git", () => {
"runner stage must install ripgrep — the coding agents Fusion drives use `rg` as their primary search tool",
);
});
/*
FNXC:DockerRun 2026-08-18-06:40:
The operator tooling the image promises must actually be in it. `gh` backs the gh-cli GitHub auth
mode, `cloudflared` backs remote access, and `tailscale` is the private-network option; each is
installed from its vendor's signed apt repository. Assert the repo wiring AND the package names, so
dropping either half (a keyring without the install, or an install whose repo line was removed) fails
here instead of at first use inside a container.
*/
test("runner stage installs gh, tailscale and cloudflared from vendor repositories", () => {
const dockerfile = readFileSync(path.join(repoRoot, "Dockerfile"), "utf8");
const runnerStage = dockerfile.slice(dockerfile.indexOf("FROM node:22-slim AS runner"));
for (const [tool, repo] of [
["gh", "https://cli.github.com/packages"],
["tailscale", "https://pkgs.tailscale.com/stable/debian"],
["cloudflared", "https://pkg.cloudflare.com/cloudflared"],
]) {
assert.ok(runnerStage.includes(repo), `runner stage must configure the ${tool} apt repository (${repo})`);
assert.match(runnerStage, new RegExp(`apt-get install[^\n]*(?:\\\n[^\n]*)*\\b${tool}\\b`), `runner stage must install ${tool}`);
}
});