Skip to content

fix: Tailscale container restart loop caused by web UI port conflict#974

Open
yyyr-p wants to merge 1 commit into
IceWhaleTech:mainfrom
yyyr-p:fix/tailscale-web-port-conflict
Open

fix: Tailscale container restart loop caused by web UI port conflict#974
yyyr-p wants to merge 1 commit into
IceWhaleTech:mainfrom
yyyr-p:fix/tailscale-web-port-conflict

Conversation

@yyyr-p

@yyyr-p yyyr-p commented Jul 12, 2026

Copy link
Copy Markdown

Summary

  • Move the LAN-facing tailscale web listener from 5252 to 5253 (5252+1) to avoid a bind conflict with tailscaled's built-in tailnet web UI, which also uses port 5252.
  • Update x-casaos.port_map to 5253 accordingly so the CasaOS app tile keeps opening the right port.

Root cause

Tailscale ships an on-device web UI ("device web interface", KB 1325) introduced in v1.56. When a user enables it for tailnet access via tailscale set --webclient (persisted as the RunWebClient pref), tailscaled starts listening on <tailnetIP>:5252 and [<tailnet-v6-ip>]:5252. Port 5252 is a hardcoded constant in client/web/web.go (const ListenPort = 5252, chosen because 5252 sits above "TSTS" on a qwerty keyboard).

The current entrypoint tries to expose a LAN-facing web UI with:

tailscale web --listen 0.0.0.0:5252

The wildcard socket *:5252 overlaps with the tailnet-IP-bound socket that tailscaled already holds, so the bind fails and tailscale web exits. Because it's the last foreground command in the entrypoint (tailscaled ... & sleep 10; tailscale web ...), the shell exits (status 0), Docker treats the container as done, and restart: unless-stopped restarts it — every ~10s indefinitely. On a live system: RestartCount grows continuously, docker events shows back-to-back die/start pairs with execDuration=10 exitCode=0.

This is user-pref-dependent, not upstream-version-dependent: users who never enable --webclient don't hit it. That is presumably why the setup worked when written on v1.90.8 in #833 and only later broke for users who turned the pref on.

Turning the pref back off with tailscale set --webclient=false is not a reliable workaround either — see tailscale/tailscale#12084 (still open). The pref changes but the socket isn't released without a tailscaled restart (independently reproduced during this investigation on v1.98.3). So choosing a non-conflicting port is the correct fix.

Why 5253 (not 8088)

The tailscale web command's documented default is localhost:8088, which would be the natural choice — but 8088 is already used by the Trilium app in this store (Apps/Trilium/docker-compose.yml, also host-networked), so two apps would collide on the same host port when both are installed.

5253 was chosen because:

  • It is unused across the entire AppStore (grep-verified against every Apps/*/docker-compose.yml).
  • It stays visually associated with the built-in 5252, making the conflict-avoidance intent obvious.

Validation

  • docker compose config -q on the modified file passes.
  • Deployed the modified compose to a live ZimaOS host running tailscale/tailscale:v1.98.3 with RunWebClient: true (i.e. the failure case):
    • Container: Up, RestartCount=0 after 60s+ (previously RestartCount=143 and growing).
    • No die/start events observed for 75s of monitoring.
    • curl http://<host-lan-ip>:5253/HTTP/1.1 200 OK from a LAN client.
    • curl http://100.100.200.1:5252/HTTP/1.1 200 OK from a tailnet peer (built-in UI unaffected — the two coexist).

Scope

Metadata-only change; no image bump (still v1.98.3), so version, update_at, and release_notes are intentionally left unchanged — matching the minimal-diff style of prior small fixes like #957.

Move the LAN-facing `tailscale web` listener from 5252 to 5253 (5252+1)
and update `port_map` to match.

Since Tailscale v1.56, `tailscaled` ships an on-device web UI ("device
web interface", KB 1325) that binds `<tailnetIP>:5252` when the
`RunWebClient` pref is enabled via `tailscale set --webclient`. The
port 5252 is a hardcoded constant in `client/web/web.go`
(`const ListenPort = 5252`).

The previous entrypoint's `tailscale web --listen 0.0.0.0:5252` then
fails to bind (`*:5252` overlaps the tailnet-IP-bound socket already
held by `tailscaled`); the `tailscale web` process exits; the shell
exits (status 0); Docker treats the container as done; and
`restart: unless-stopped` restarts it — every ~10s indefinitely.

This is user-pref-dependent, not upstream-version-dependent: users who
never enabled `--webclient` don't hit it. That's why the setup in IceWhaleTech#833
worked when written on v1.90.8 and only later broke for users who
turned the pref on. `tailscale set --webclient=false` doesn't reliably
release the socket either (tailscale/tailscale#12084, still open), so
choosing a non-conflicting port is the correct fix.

Port 5253 was chosen over `tailscale web`'s default 8088 because 8088
is already used by the Trilium app (`Apps/Trilium/docker-compose.yml`,
also host-networked). 5253 is unused across the entire AppStore
(grep-verified) and stays visually associated with the built-in 5252.

Verified on ZimaOS with tailscale/tailscale:v1.98.3 and
`RunWebClient: true` (the failure case):
- `docker compose config -q` passes.
- After deploy: `Up`, `RestartCount=0` for 60s+ (previously 143).
- No `die`/`start` events observed for 60s.
- LAN `curl http://<host>:5253/` returns HTTP 200.
- Tailnet `curl http://100.100.200.1:5252/` still returns HTTP 200
  (built-in UI unaffected).
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant