Deployment
Two deploy paths: infrastructure (NixOS config changes) and application code (per-service repos).
Infrastructure
Pushing to main triggers .github/workflows/deploy.yaml which runs deploy-rs over Tailscale to rebuild NixOS on the target machine.
# From the dev shell (preferred)
nix develop
deploy .#terebithia
deploy .#prattle
# Manual one-off
nix develop .#default --command deploy --ssh-user kierank .#terebithia
Builds happen on an arch-matched CI runner; only the closure diff is copied to the target.
Application Code
Each service repo has a minimal workflow calling the reusable .github/workflows/deploy-service.yml. On push to main:
- Connects to Tailscale (
tag:ci) - SSHes as the service user (e.g.,
cachet@terebithia) via Tailscale SSH - Snapshots the SQLite DB (if
db_pathis provided) git pull+ install (bun install --frozen-lockfile, or whateverinstall_commandsays) +sudo systemctl restart- Health check (HTTP URL or systemd status fallback)
- Auto-rollback on failure (restores DB snapshot + reverts to previous commit)
Per-app workflow. Copy and change the with: values:
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
uses: taciturnaxolotl/dots/.github/workflows/deploy-service.yml@main
with:
service: cachet
health_url: https://cachet.dunkirk.sh/health
db_path: /var/lib/cachet/data/cachet.db
secrets:
TS_OAUTH_CLIENT_ID: ${{ secrets.TS_OAUTH_CLIENT_ID }}
TS_OAUTH_SECRET: ${{ secrets.TS_OAUTH_SECRET }}
Omit health_url to fall back to systemctl is-active. Omit db_path for stateless services.
Set install_command for anything that is not bun, e.g. env UV_PYTHON_DOWNLOADS=never uv sync --frozen for python/uv services (see botme).
Adding a new service
- Create a module in
modules/nixos/services/using mkService or a custom module - Register secrets in
secrets/secrets.nixand encrypt with agenix - Enable in the target machine’s
default.nix - Add a deploy workflow to the app repo (if it has one)
See modules/nixos/services/cachet.nix for a minimal mkService example.