Skip to content

[Bug]: 0.1.2 → 0.1.3 server update rolls back with EADDRINUSE when Tailscale Serve already publishes port 3773 (new 0.0.0.0 default bind) #27386

Closed as duplicate

Description

@LightCC

Assisted by Claude Opus 5.5 using T3 Code.

Summary

0.1.3 changed the server's default bind from 127.0.0.1 to 0.0.0.0. On a headless Linux host where tailscale serve already publishes the T3 port (3773), Tailscale holds <tailnet-ip>:3773. The 0.1.3 candidate's listen(0.0.0.0:3773) fails with EADDRINUSE, the candidate exits, and the service launcher rolls back to 0.1.2. Every retry fails the same way. The desktop client then shows "update failed" for that environment, and the message stays after the remote has been updated some other way.

Two issues:

  1. Main: the server update can't succeed on a Tailscale Serve host unless T3CODE_HOST=127.0.0.1 is set first. The error doesn't mention the bind-address change.
  2. Minor: the Windows desktop app kept showing the remote update as failed after the remote was updated to 0.1.3 manually. Restarting the app cleared it.

Environment

Remote server ("Mirror") Client ("light-star-one")
OS Linux Mint 22.1 (Xia), Cinnamon, kernel 6.8.0-147-generic Windows 11 Pro 10.0.26200 (build 26200)
T3 Headless background service (t3code.service, systemd user unit, t3 __service-launcher). 0.1.2 → 0.1.3 T3 Code (Beta) desktop 0.1.3 (T3 Code (Beta).exe ProductVersion 0.1.3.0); t3 CLI v0.1.3
Tailscale 1.104.2 1.104.2
Connection Desktop connects to Mirror over the tailnet through Tailscale Serve HTTPS

Mirror's Tailscale Serve config (hostname redacted):

https://<mirror>.<tailnet>.ts.net:3773 (tailnet only)
|-- / proxy http://127.0.0.1:3773
https://<mirror>.<tailnet>.ts.net:8443 (tailnet only)
|-- / proxy http://127.0.0.1:3773

Before the update, the systemd unit set no host (only T3CODE_HOME and T3_BOOT_SERVICE_UNIT), so 0.1.2 used its default.

Steps to reproduce

  1. Run a 0.1.2 headless service on Linux with the default host, and publish it with tailscale serve --https=3773 http://127.0.0.1:3773.
  2. Trigger the server update to 0.1.3 from the desktop client, or let the service self-update.
  3. The candidate fails to start and the launcher rolls back to 0.1.2. Retries fail the same way.

Evidence

Source: ~/.t3/userdata/logs/boot-service.log on Mirror (the systemd unit's stdout/stderr). Stack frames, pairing tokens and QR codes removed.

0.1.2 bound loopback by default. Every 0.1.2 start shows:

[22:59:36.166] INFO (#227): Listening on http://127.0.0.1:3773

The 0.1.3 candidate tries 0.0.0.0 and hits the port Tailscale holds. The 0.1.2 service then comes back on loopback:

[23:22:11.574] INFO (#303699): Server update prepared; handing off to the service launcher.
  { updateId: 'ade5dc4b-…', targetVersion: '0.1.3', runtimePath: '/home/<user>/.t3/runtime/versions/0.1.3/t3' }
[23:22:14.332] ERROR (#2): ServeError:
    at Server.onError (file:///home/<user>/.t3/runtime/versions/0.1.3/t3:153776:19)
    at server.runServerCommand (file:///home/<user>/.t3/runtime/versions/0.1.3/t3:421304:203) {
  [cause]: Error: listen EADDRINUSE: address already in use 0.0.0.0:3773
}
[23:22:15.399] INFO (#6432): Listening on http://127.0.0.1:3773

The same handing off → EADDRINUSE 0.0.0.0:3773 pair appears for all 6 attempts: 23:22:11, 01:43:25, 01:43:44, 01:43:52, 01:45:27 and 09:06:01 (2026-10-28/29, local time EDT). During this period, ~/.t3/runtime/service-state.json showed status: rolled-back.

Who holds the port. ss -ltnp on Mirror after the fix:

LISTEN  100.x.x.x%tailscale0:3773
LISTEN  [fd7a:…]%tailscale0:3773
LISTEN  127.0.0.1:3773   users:(("node-MainThread",pid=…))

The tailscale0 sockets belong to tailscaled. ss run without root doesn't show their owner, but they match the Serve config above.

A wildcard 0.0.0.0:3773 bind conflicts with the tailscaled socket on the tailnet IP. Binding 127.0.0.1:3773 doesn't.

Workaround (confirmed)

Pin the host with a systemd drop-in, then update from the CLI:

# ~/.config/systemd/user/t3code.service.d/host.conf
[Service]
Environment=T3CODE_HOST=127.0.0.1
systemctl --user daemon-reload
t3 update -y

Result (same log):

[09:22:26.956] INFO (#6518): Listening on http://127.0.0.1:3773
[09:22:27.197] INFO (#6519): Removed unused t3 runtimes.
  { versions: [ '0.1.2' ] }

service-state.json now shows {"protocol":3,"activeVersion":"0.1.3"}.

Stale "update failed" in the desktop client (minor)

After Mirror was on 0.1.3 and healthy, the Windows desktop app (0.1.3) still showed the Mirror update as failed. Neither machine had that state on disk: there was no failure in Mirror's service-state.json or logs, and nothing in the desktop's %USERPROFILE%\.t3\userdata logs or SQLite stores. So it seems to be in-memory client state. Restarting the desktop app cleared it. Expected behavior: the client rechecks the remote's activeVersion on reconnect and clears the failure.

Suggested fixes

  • Option A: keep 127.0.0.1 as the default for the background service. At minimum, use it when tailscale serve already targets the port.
  • Option B: if 0.0.0.0 stays the default, have the pre-update check (or the candidate's failure report) detect EADDRINUSE and name the cause and fix. For example: "port 3773 is in use, probably by Tailscale Serve; set T3CODE_HOST=127.0.0.1." Today the client only gets a generic rollback.
  • Call out the default-bind change in the 0.1.3 release notes, since it's a breaking change for Tailscale Serve setups.

Activity

  1. ranvier2d2 commented on Oct 30, 2026

    @ranvier2d2
    Contributor

    Same root cause on the macOS desktop app, not only Linux service installs.

    Version: T3 Code (Nightly) desktop 0.1.5-nightly.20261029.3877 (darwin arm64), where the bug happened. Still the expected behavior on 0.1.6-nightly.20261030.3917, since the desktop bind host comes from the exposure mode. The triage CLI was t3 0.1.5.

    Setup

    • Desktop Connections set to network-accessible (serverExposureMode: "network-accessible"). The desktop then binds 0.0.0.0 (apps/desktop/src/backend/DesktopServerExposure.ts, DESKTOP_LAN_BIND_HOST).
    • App launched with T3CODE_PORT=3773 by a login agent.
    • A leftover manual Tailscale Serve entry https://<host>.<tailnet>.ts.net:3773 → http://127.0.0.1:3775. Its target was dead, but Tailscale's network extension (root) still listens on <tailnet-ip>:3773.

    What happened
    On login the backend crash-looped 9 times in 45 s:

    ServeError: … [cause]: Error: listen EADDRINUSE: address already in use 0.0.0.0:3773
    

    The startup dialog says the server "could not start (code=1)" and gives no hint that a Tailscale Serve listener on the same port number is the cause. On macOS, netstat -anv shows the owner as io.tailscale.ipn. lsof run as the user shows nothing on 3773, which makes this hard to diagnose.

    Knock-on effect (may belong in #23372 / #22820): an auto-update relaunched the app without the env var, so the backend fell back to port 3775. It then spent 201 s in migration 73_ActiveTurnItemIndexes (two CREATE INDEX on orchestration_v2_projection_turn_items, 170 s + 31 s, on a 23.5 GB statev2.sqlite), with no window. The user quit after 55 s, and the backend kept running and held the owner lock. The next two launches showed "This T3 home is unavailable … Another T3 Code server, such as the background service, is using this data directory". No other server or service existed.

    Workaround that works: Connections → local-only, keep built-in Tailscale Serve on, and set tailscaleServePort: 3773 in desktop-settings.json. The backend binds 127.0.0.1:3773, and T3 publishes https://…ts.net:3773 → http://127.0.0.1:3773 itself. Both coexist with no conflict, and the tailnet URL and ACLs don't change.

    Suggestions

    1. On EADDRINUSE for a wildcard bind, check tailscale serve status for the same port and say so in the error. At least point out that local-only mode avoids it.
    2. The desktop UI doesn't seem to expose tailscaleServePort. Making it settable, or documenting it, would let people keep an existing :3773 tailnet URL.
    3. Show migration progress (or at least a window) during long startup migrations, and keep the ownership dialog from blaming "another server" when the lock holder is this app's own still-running backend.

    Related PRs

    Diagnosed and written with Claude Opus 5.5 (Claude Code) via t3 triage.

  2. LightCC commented on Oct 31, 2026

    @LightCC
    Author

    @ranvier2d2 - Yeah, I ran across that "another server is using the data directory" somewhere along the way as well

  3. juliusmarminge commented on Nov 12, 2026

    @juliusmarminge
    Member

    This is the same problem as #25161, so I'm closing it in favour of that one. If it is different, say how and it will be reopened.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions