Do not hold boot hostage for the tailnet join

Review caught that the join unit was Type=oneshot wanted by
multi-user.target: target units implicitly gain After= for their Wants=,
so an offline machine's boot -- SDDM included -- stalled for the full
10-minute timeout, and once timed out the join never retried within that
boot. Type=simple counts as started the moment it forks, so boot proceeds
while the join retries in the background for as long as the boot lasts.
Cleanup moves inside the script, sequenced after tailscale up succeeds,
which preserves the retry-across-boots behavior: key and unit survive a
boot with no connectivity, and the key never outlives a successful join.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xc74h4WSYpuYPGuYbFAR1q
This commit is contained in:
David Heinemeier HanssonandClaude Fable 5 committed 2026-08-07 15:09:11 +02:00
1 parent d33487a09e
commit 2a46fef776
4 files changed
+46 -26

No files matched your search

+16 -10
View File
@@ -40,22 +40,28 @@ offline-capable.
`tailscale up` needs a running tailscaled, and there is no systemd in the
chroot. So the installer only stages: the key at `/etc/tailscale/authkey`
(root, `0600`), `tailscaled.service` enabled, and a oneshot
(root, `0600`), `tailscaled.service` enabled, and an
`omarchy-tailscale-join.service` that runs `tailscale up --auth-key
file:/etc/tailscale/authkey` on first boot.
"When the network is available" lives in the unit, not the installer:
`After=network-online.target` for ordering, plus an in-unit retry loop
(`until tailscale up …; do sleep 10; done` under `TimeoutStartSec=10min`),
because network-online can be reached before there is real connectivity.
The systemd success/failure semantics do the rest for free:
(`until tailscale up …; do sleep 15; done`), because network-online can be
reached before there is real connectivity.
- On success, `ExecStartPost` removes the key and disables the unit — the key
never outlives the join, and the unit never runs again.
- On failure (no network, tailscaled not ready, key rejected), ExecStartPost
is skipped: the key stays, the unit stays enabled, and the join retries on
every boot until it succeeds. A machine installed offline joins whenever it
first gets connectivity.
The unit is `Type=simple`, not oneshot, and that is load-bearing: target
units implicitly gain `After=` for their `Wants=`, so a oneshot wanted by
`multi-user.target` holds the whole boot — SDDM included — hostage until the
join finishes or times out, and once timed out it never retries within that
boot. (Caught by review.) Type=simple counts as started the moment it forks:
boot proceeds normally while the join retries in the background for as long
as the boot lasts.
Cleanup is sequenced inside the script, after `tailscale up` succeeds — the
key is removed and the unit disables itself, so the key never outlives the
join and the unit never runs again. On a boot that never gets connectivity
both survive (`ConditionPathExists` gates re-runs on the key), so a machine
installed offline joins on the first boot that can.
The machine appears on the tailnet under the hostname from
`user_configuration.json` — nothing extra to configure.