SSH into one stable Tailscale node and reach your whole tailnet from it via short per-host functions + sshfs mounts. A riced Arch dev container with sshd, a stateful home, and declarative .env auth. See README.md and ROAMING.md. |
||
|---|---|---|
| dotfiles | ||
| scripts | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| entrypoint.sh | ||
| nvm.zsh.snippet | ||
| README.md | ||
| roaming-zshrc.snippet | ||
| ROAMING.md | ||
Homescale
Your fleet, one word away · a Tailscale-native roaming home
"I'd far rather be happy than right any day." And you're far more likely to be both when your whole fleet answers from a single container you
sshinto.
Homescale turns a riced Arch dev container into a roaming home for your fleet: one stable Tailscale node you SSH into, from which every host on your tailnet is a single word away. It holds the keyring; you carry nothing but a terminal. Run a coding agent inside it and it becomes a fleet control plane with hands.
The IDE is still under the hood — colorscheme, LSP, a git-snapshotted home. It's just no longer the point. The point is home.
(Tailscale today → Headscale later: the "home" you scale.)
Use it
ssh <user>@<node>.<your-tailnet>.ts.net
You land in your full stateful home. Each registered host is then just a verb:
web uptime # runs on the host named "web", over the tailnet
db sudo systemctl status pg # remote sudo prompts — the boundary holds
edge journalctl -u caddy # any registered host
mount_host db # sshfs its filesystem → /workspace/db
roaming-status # what's registered
Each host becomes a shell function. Pipes/redirects run locally, exactly like plain ssh — quote the whole pipeline to run it remote.
Onboard a host
add_new_machine.sh NAME NAME.<your-tailnet>.ts.net <user> /
roaming-reload
Three things that bite, up front:
- Key-only hosts (
PasswordAuthentication no) can't be bootstrapped byssh-copy-id. Pre-install Homescale's~/.ssh/id_ed25519.pubvia a path the host already trusts — keylesstailscale sshis ideal. - Custom ssh port? Add a
Host … Port <PORT>block to~/.ssh/config(the roaming engine uses plain ssh). - Aliases don't travel —
NAME <cmd>is non-interactive ssh. Use real binaries or~/binscripts.
How it's built
| Piece | What |
|---|---|
ts-nvimide |
Tailscale sidecar — stable node (no ?ephemeral), tag:nvimide |
nvimide |
the dev image sharing the sidecar's netns, running sshd as init |
| home volume | stateful home (snapshots, tools, .ssh) |
| auth | declarative via .env (NVIMIDE_USER / NVIMIDE_PASSWORD / NVIMIDE_SSH_PUBKEY) |
cp .env.example .env # fill TS_OAUTH_CLIENT_SECRET, NVIMIDE_SSH_PUBKEY, TS_TAILNET …
docker compose up -d --build
The committed compose uses a managed home volume so it works out of the box. To pin it to an existing/migrated volume, override
nvimide-homein a local (gitignored)docker-compose.override.yml.
Tailnet ACL (lives in your Tailscale console, not this repo)
A grant for plain-TCP reach + two ssh rules — a separate section from
grants, needed in both directions:
{ "src": ["tag:nvimide"], "dst": ["*"], "ip": ["*"] } // reach (plain ssh)
{ "src": ["tag:nvimide"], "dst": ["*"], "users": ["<user>"], "action": "accept" } // tailscale ssh OUT
{ "src": ["*"], "dst": ["tag:nvimide"], "users": ["<user>"], "action": "accept" } // tailscale ssh IN — "Go Home!"
Deeper
ROAMING.md— the roaming engine: registry format, host-functions, sshfs mounts, the gotchas (host-key persistence, theCAP_SYS_CHROOTsshd needs, the sidecar healthcheck that must not gate on the app's:22).
MIT — fork it, name your node something silly, make it home.