Compacts the design discussion into a reference: the workflow model (pure Remote-SSH by default, opportunistic local clones for self-contained python+uv projects only), how to run the script, the home-machine prerequisite (Tailscale SSH), and the gotchas hit while building it (PATH issues after su, stale sudo group membership, the token-paste-into-shell bug, the spurious-space host alias bug, and the tailnet-wide MagicDNS requirement) so they don't need re-debugging. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|---|---|---|
| bootstrap.sh | ||
| README.md | ||
travel laptop bootstrap
A disposable, extremely light Debian laptop for working on the road. If it's
lost, stolen, or dropped in the sea: install Debian, fetch bootstrap.sh,
run it, give it a couple of credentials, and you're back to full working
order in minutes.
The idea
- The travel laptop stays minimal: vim, tmux, mosh, tailscale, VS Code
(+ Remote-SSH extension),
uv, and a locked-down browser. No language runtimes, no Docker, no databases. - Default workflow is pure Remote-SSH, nothing local. VS Code connects over Tailscale to your home machine (or another tailnet box) and edits that machine's checkout directly. Actually running/building/testing code always happens there too - never on the travel laptop - so a project that generates tens of GB of logs/DB state on a "heavy" run never becomes the travel laptop's problem, regardless of whether that particular project is big or small.
- Local clones are opportunistic, not default. Only migrate a repo to
the laptop itself if it's genuinely self-contained (Python +
uv, no services/DB/heavy runtime state) and you specifically want it editable offline. Everything else (e.g. anything needing a distrobox container, a full toolchain, Docker) stays remote-only, always. - Offline mode = edit + commit only. Building/running resumes once you're back online and reconnect via Remote-SSH.
- Idempotent, not transactional: every step checks its own state before acting, so the script is safe to run any number of times. If a step fails, fix the underlying issue and re-run - already-done steps skip, nothing needs to be undone.
Running it
On a fresh Debian install, once you're on the network:
sudo apt update && sudo apt install -y curl
curl -fsSL https://git.brainpaingames.com/tommy/infra/raw/branch/main/bootstrap.sh -o bootstrap.sh
chmod +x bootstrap.sh
./bootstrap.sh
It prompts for:
- Forgejo instance URL and a device/key label (remembered across
runs in
~/.config/travel-bootstrap/configand offered as the default next time - just hit Enter to reuse them). - Tailscale login (interactive, opens a URL to approve the device).
- A Forgejo access token, used once to register a fresh SSH keypair via
the API, never stored. Get one at: your Forgejo instance -> avatar (top
right) -> Settings -> Applications -> Manage Access Tokens -> grant
write:userscope -> Generate Token -> copy it immediately, it's shown once. When prompted, paste it and press Ctrl-D (not Enter) to submit.
What it does, in order: installs packages, joins Tailscale, regenerates
~/.ssh/config from the live tailnet peer list (a wildcard entry covering
the whole tailnet's DNS suffix, plus one named Host alias per peer -
regenerated fresh every run, so new peers just show up next time), registers
the new SSH key to Forgejo, and hardens Firefox (forced incognito, no
history, forced uBlock Origin). Ends with a summary and, if everything
succeeded, a concrete "here's what to do next" block listing the tailnet
hosts it found.
Connecting to a project
Open VS Code -> Ctrl+Shift+P -> Remote-SSH: Connect to Host... -> pick
the home machine's alias from the list -> File > Open Folder, same as
opening a folder locally, just on the remote filesystem.
For a project living inside a distrobox container on the remote:
Remote-SSH gets you the host's shell, not the container - run
distrobox enter <name> in the terminal same as you would locally.
Nothing about arriving over SSH changes that. Tighter integration (VS Code
extensions actually running inside the container) is possible later via
the Dev Containers extension's "Attach to Running Container," launched
from within an already-open Remote-SSH window - not set up yet.
On the home machine side
One prerequisite that lives outside this script, on whichever machine you Remote-SSH into:
sudo tailscale set --ssh
This lets Tailscale itself authorize SSH logins via your tailnet identity,
so the travel laptop's key never needs to be manually added to
~/.ssh/authorized_keys there. (The SSH key this script registers to
Forgejo is for a different purpose - git operations against Forgejo, a
separate trust relationship from logging into the home machine.)
Gotchas hit while building this (so they don't get re-debugged)
usermod/visudo: command not found aftersu- not missing, just not onPATH. Usesu -(with the dash) for a proper root login shell, or call by full path (/usr/sbin/usermod,/usr/sbin/visudo).- Group membership looks correct but
sudostill refuses - group changes only apply to new login sessions. Check with plainid(no args) in the actual session you're testing in; ifsudoisn't listed there even thoughid <user>shows it, the session is stale - reboot or fully re-login. - Forgejo API call fails with a vague error - the script reports the
real HTTP status now (401 = bad/expired token, 403 = missing
write:userscope, 404 = check the instance URL). - A pasted token ends up executed as a shell command - some "copy
token" buttons include a trailing newline. A plain
readstops at that newline and leaves the rest of the paste sitting in the terminal's input buffer, which the shell then runs once the script exits. Fixed by reading raw stdin until EOF (paste, then Ctrl-D) instead of stopping at the first newline. - A generated
Hostalias had a space in it - Tailscale's rawHostNamefield is whatever the OS reports (an Android phone's name can be e.g."Pixel 9"), which breaks unquoted in an SSH configHostline. Fixed by deriving the alias from the already-sanitizedDNSNameinstead. .ts.netnames don't resolve, even a device resolving its own name- MagicDNS has to be switched on tailnet-wide (Tailscale admin console ->
DNS tab), separately from any per-device setting. Each device always
gets a
.ts.netname assigned regardless of whether this is on - it just won't actually resolve until it is.
- MagicDNS has to be switched on tailnet-wide (Tailscale admin console ->
DNS tab), separately from any per-device setting. Each device always
gets a
Deliberately deferred / manual
- Disk encryption (LUKS) - a Debian-installer-time choice, not something this script can retrofit. Worth doing given this machine holds an SSH key, even if the disk otherwise stays close to empty.
- Browser choice: Firefox, for its declarative
policies.json. - Window manager: left as whatever Debian's installer gives you; a minimal WM (sway/i3) would cut RAM/disk further if that becomes worth it.