diff --git a/.changeset/tailscale-daemon-container.md b/.changeset/tailscale-daemon-container.md index d76da16da4..2509fd3c88 100644 --- a/.changeset/tailscale-daemon-container.md +++ b/.changeset/tailscale-daemon-container.md @@ -4,4 +4,4 @@ summary: Fix Tailscale remote access failing with "process exited 1" in the Docker image. category: fix -dev: The image shipped the `tailscale` CLI but never ran `tailscaled`, so the `tailscale funnel ` spawn died immediately. A new `scripts/docker-entrypoint.sh` best-effort starts the daemon in userspace-networking mode (no NET_ADMIN/tun caps needed; disable with `FUSION_DISABLE_TAILSCALED=1`), and `/var/lib/tailscale` symlinks into `/home/node/.tailscale` so login state persists across container recreates. `evaluateRemoteLifecycle` now preflights daemon reachability and backend state via `tailscale status --json` instead of only `which tailscale`, so an unreachable, logged-out, or stopped backend reports an actionable `runtime_prerequisite_missing` reason. +dev: The image shipped the `tailscale` CLI but never ran `tailscaled`, so the `tailscale funnel ` spawn died immediately. A new `scripts/docker-entrypoint.sh` starts the daemon in userspace-networking mode (no NET_ADMIN/tun caps needed) when opted in with a leading `--tailscale` argument or `FUSION_TAILSCALE=1`; the flag is stripped before the CLI runs. `/var/lib/tailscale` symlinks into `/home/node/.tailscale` so login state persists across container recreates. `evaluateRemoteLifecycle` now preflights daemon reachability and backend state via `tailscale status --json` instead of only `which tailscale`, so an unreachable, logged-out, or stopped backend reports an actionable `runtime_prerequisite_missing` reason. diff --git a/Dockerfile b/Dockerfile index 6a1b32a8d3..59c3de510c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -204,8 +204,9 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ # 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 best-effort starts tailscaled and then `exec`s -# that same absolute-path node invocation with CMD verbatim, so PID 1, signal handling, and every -# documented `docker run ... dashboard --host 0.0.0.0` argument list behave exactly as before. +# FNXC:DockerRun 2026-08-23-02:03: The wrapper script consumes its own opt-in `--tailscale` flag and +# then `exec`s that same absolute-path node invocation with the REMAINING args verbatim, so PID 1, +# signal handling, and every documented `docker run ... dashboard --host 0.0.0.0` argument list behave +# exactly as before. ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] CMD ["dashboard", "--host", "0.0.0.0"] diff --git a/docs/docker.md b/docs/docker.md index 2e99630463..59aca2387c 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -80,6 +80,44 @@ outside the container while a login is in flight; it is short-lived and validate 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 `, 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: + +```bash +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: + +```bash +docker exec -it 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: diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index 70597b9610..bc26ca5d2f 100755 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -1,10 +1,17 @@ #!/bin/sh -# FNXC:DockerRun 2026-08-23-02:03: -# Start `tailscaled` before the dashboard, because the image shipping the `tailscale` CLI is not -# enough to make the remote-access feature work. Fusion's tunnel spawns a bare `tailscale funnel -# `, which needs a running daemon on the DEFAULT socket; with no daemon it dies instantly with -# "failed to connect to local tailscaled" and exit 1, surfacing in the UI as an unexplained process -# failure (operator report: "starting tailscale tunnel in container is failing with process exited 1"). +# FNXC:DockerRun 2026-08-23-02:13: +# Optionally start `tailscaled` before the dashboard, because the image shipping the `tailscale` CLI +# is not enough to make the remote-access feature work. Fusion's tunnel spawns a bare +# `tailscale funnel `, which needs a running daemon on the DEFAULT socket; with no daemon it +# dies instantly with "failed to connect to local tailscaled" and exit 1, surfacing in the UI as an +# unexplained process failure (operator report: "starting tailscale tunnel in container is failing +# with process exited 1"). +# +# The daemon is OPT-IN via a leading `--tailscale` argument (or `FUSION_TAILSCALE=1`), not on by +# default: most containers never use remote access, and a background daemon they did not ask for is +# a process, a listening socket, and an identity in someone's tailnet. The flag is consumed here and +# STRIPPED from the argument list, so everything after it stays a normal Fusion CLI invocation and +# `docker run fusion --tailscale dashboard --port 8080` behaves exactly like the documented form. # # Userspace networking (`--tun=userspace-networking`) is deliberate: it needs neither `NET_ADMIN` nor # `/dev/net/tun`, so the documented `docker run` keeps working unchanged, and it is sufficient for @@ -12,8 +19,8 @@ # proxy listeners are the standard userspace-mode escape hatch for outbound tailnet access, which has # no route out otherwise. # -# Startup is BEST-EFFORT and never fails the container: an operator who does not use Tailscale must -# still get a dashboard. Set FUSION_DISABLE_TAILSCALED=1 to skip it entirely. +# Startup is BEST-EFFORT and never fails the container: a daemon that will not start must still leave +# the operator with a dashboard, and the tunnel preflight reports the unusable backend by itself. # # Login is NOT automated here — `tailscale up` requires an interactive auth URL or an operator's auth # key, so the daemon comes up logged-out and the operator authenticates once. State lives under @@ -21,7 +28,24 @@ # `-v :/home/node` mount persists that login across container recreates. set -e -if [ "${FUSION_DISABLE_TAILSCALED:-0}" != "1" ] && [ -x /usr/sbin/tailscaled ]; then +tailscale_enabled="${FUSION_TAILSCALE:-0}" + +# Rotate the argument list, dropping the flags this wrapper owns. The shift/append idiom is used +# rather than string concatenation so arguments containing spaces survive intact. +argc=$# +i=0 +while [ "$i" -lt "$argc" ]; do + arg="$1" + shift + case "$arg" in + --tailscale) tailscale_enabled=1 ;; + --no-tailscale) tailscale_enabled=0 ;; + *) set -- "$@" "$arg" ;; + esac + i=$((i + 1)) +done + +if [ "$tailscale_enabled" = "1" ] && [ -x /usr/sbin/tailscaled ]; then if [ ! -S /var/run/tailscale/tailscaled.sock ]; then /usr/sbin/tailscaled \ --tun=userspace-networking \