fix: make container tailscaled opt-in via a --tailscale entrypoint flag
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>
This commit is contained in:
@@ -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 <port>` 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 <port>` 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.
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 <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:
|
||||
|
||||
```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 <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:
|
||||
|
||||
@@ -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
|
||||
# <port>`, 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 <port>`, 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 <vol>:/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 \
|
||||
|
||||
Reference in New Issue
Block a user