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>
128 lines
6.4 KiB
Markdown
128 lines
6.4 KiB
Markdown
# 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:
|
|
|
|
```bash
|
|
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/config` and 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:user` scope -> 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:
|
|
|
|
```bash
|
|
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** after `su` - not missing, just
|
|
not on `PATH`. Use `su -` (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 `sudo` still refuses** - group
|
|
changes only apply to *new* login sessions. Check with plain `id` (no
|
|
args) in the actual session you're testing in; if `sudo` isn't listed
|
|
there even though `id <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:user` scope, 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 `read` stops 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 `Host` alias had a space in it** - Tailscale's raw
|
|
`HostName` field is whatever the OS reports (an Android phone's name can
|
|
be e.g. `"Pixel 9"`), which breaks unquoted in an SSH config `Host` line.
|
|
Fixed by deriving the alias from the already-sanitized `DNSName` instead.
|
|
- **`.ts.net` names 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.net` name assigned regardless of whether this is on - it
|
|
just won't actually resolve until it is.
|
|
|
|
## 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.
|