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.
99 lines
3.5 KiB
Markdown
99 lines
3.5 KiB
Markdown
# 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 `ssh` into.
|
|
|
|
**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
|
|
|
|
```bash
|
|
ssh <user>@<node>.<your-tailnet>.ts.net
|
|
```
|
|
|
|
You land in your full stateful home. Each registered host is then just a verb:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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 by
|
|
`ssh-copy-id`. Pre-install Homescale's `~/.ssh/id_ed25519.pub` via a path the
|
|
host already trusts — keyless `tailscale ssh` is 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 `~/bin` scripts.
|
|
|
|
---
|
|
|
|
## 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`) |
|
|
|
|
```bash
|
|
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-home` in 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**:
|
|
|
|
```jsonc
|
|
{ "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, the `CAP_SYS_CHROOT` sshd needs, the
|
|
sidecar healthcheck that must not gate on the app's `:22`).
|
|
|
|
---
|
|
|
|
*MIT — fork it, name your node something silly, make it home.*
|