nix files for:
- my mac
- my remote exe.dev machine
Everyday deploys are in place — they update the running VM without recreating it, so the Tailscale node, the public URL, and all on-disk state are preserved, and only changed services reload (unchanged ones keep their PIDs):
- From your Mac:
nix run .#deploy. Builds the system generation, ships its closure to the box over Tailscale, and runs<gen>/activate switch. The Mac is aarch64 and the box is x86_64, so the build is realised in the box's own store. - From CI:
.github/workflows/deploy.yamlruns after a greenbuildonmasterand is self-deciding:-
no VM tagged
computer→ itexe.dev news one from the built image (bootstrap / disaster recovery), passing the backup secrets so the box can restore from B2 on boot. This is the only path that usesRESTIC_PASSWORD/B2_ACCOUNT_KEY(CI secrets). -
VM exists → it activates in place (
nix run .#deploy) with no app secrets — RESTIC/B2/pilegram already live on the box. This path needs only tailnet access: a Tailscale OAuth client taggedtag:ci(client id inline indeploy.yaml, secret in theTS_OAUTH_SECRETrepo secret) and a policy rule lettingtag:ciSSH the computer node:
-
Roll back with sudo nix-env -p /nix/var/nix/profiles/system --rollback then
re-run activate. sudo <gen>/activate test is a dry run that prints the
overlay/reload plan and changes nothing.
Base changes still take effect only through a create (s6-overlay / nix / kernel
layer, the init/mount wrapper, image.env): they're excluded from in-place
activation, so they land when CI next has to create a VM — or when you rm the VM
to force a fresh one. After a create, the next deploy re-establishes the latest
generation, and the base init-wrapper hands off to it on exe.dev restart.
One-time steps that place secrets; backups persist them across recreations
(confirm a snapshot ran: cat /var/log/backup-cron/current). nikita's ssh key
(~/.ssh) and the tailscale authkey are both backed up, so neither is
re-placed on recreation. Each tailscale node's state lives in its own statedir
on the persistent disk but isn't backed up, so a fresh machine registers new
nodes; use an ephemeral key so retired ones auto-clean (see step 3).
-
Create the VM with the backup env vars below attached.
-
Generate nikita's per-machine ssh key and register it with GitHub as both auth and signing key:
ssh-keygen -t ed25519 gh ssh-key add ~/.ssh/id_ed25519.pub --title exedev --type authentication gh ssh-key add ~/.ssh/id_ed25519.pub --title exedev --type signing -
Create the tailscale secret: an OAuth client with the Keys → Auth Keys: write scope, tagged
tag:computer. The tailnet policy must define the tag and allow ssh into it:"tagOwners": { "tag:computer": ["autogroup:admin"] }, "ssh": [{ "action": "accept", "src": ["ngalaiko@github"], "dst": ["tag:computer"], "users": ["nikita"] }], // required for `funnel = true` serve entries (the public ingress). "nodeAttrs": [{ "target": ["tag:computer"], "attr": ["funnel"] }]
Place the secret on the machine (ephemeral, so retired VMs' nodes auto-clean):
sudo mkdir -p /var/lib/tailscale sudo sh -c 'umask 077; printf %s "tskey-client-…?preauthorized=true&ephemeral=true" > /var/lib/tailscale/authkey'Reboot (or run the per-node
tailscale upfrommodules/exedev/services/tailscale.nixby hand), thentailscale ssh nikita@computerover the tailnet. The authkey is backed up so you don't re-place it, and it registers every node. Each tailscaled keeps its node key (and, for the ssh node, its SSH host keys) in its own persistent statedir (not backed up), so a fresh machine registers new nodes; the ephemeral key lets retired ones auto-remove once offline — no manual cleanup. -
Enable HTTPS Certificates (admin console → DNS → Enable HTTPS, needs MagicDNS on). Required to provision the
*.ts.netcerts. Thecomputernode'stailscale-serveservice re-asserts this on every boot:https://computer.<tailnet>.ts.net/<tenant>/→ ingress, public via Funnel (needs thenodeAttrsabove). Unauthenticated — see the note inhosts/exedev/default.nix. This one is named after the node, so on a recreation where the retired ephemeral node hasn't dropped yet Tailscale may suffix it (computer-1) until the stale one is culled; exe.dev's public share is the stable public path if that matters.
-
Place the Telegram gateway (pilegram) secrets — the bot token and your allow-listed Telegram user id(s) — so the
assistant-gatewayservice can start. They stay out of this (public) repo and the image; the file lives under the assistant home and is backed up, so it's restored on recreation:sudo install -d -o 2001 -g 2001 -m 700 /var/lib/assistant/.config/pilegram sudo sh -c 'umask 077; printf "TELEGRAM_BOT_TOKEN=%s\nPILEGRAM_ALLOW=%s\n" "123456:AA..." "111222333" > /var/lib/assistant/.config/pilegram/env' sudo chown 2001:2001 /var/lib/assistant/.config/pilegram/envPILEGRAM_ALLOWis comma-separated numeric Telegram user ids. In BotFather, enable Threaded Mode so each Telegram topic is its own agent session. The service retries every 10s until the file exists.
We have to store it outside of the machine to be able to restore everything else on startup.
| Variable | Description |
|---|---|
RESTIC_REPOSITORY |
B2 restic repo, e.g. b2:backups:exedev |
RESTIC_PASSWORD |
restic repo encryption password |
B2_ACCOUNT_ID |
B2 key id |
B2_ACCOUNT_KEY |
B2 application key (scope to the bucket) |