My consolidated Nix configuration for macOS and NixOS systems, including a few services.
- Voyager: macOS (aarch64-darwin)
- Cassini: macOS (aarch64-darwin)
- Kepler: NixOS (x86_64-linux, headless)
- Cubesat: NixOS (x86_64-linux, headless)
# macOS systems (nix-darwin)
darwin-rebuild build --flake .#voyager
darwin-rebuild switch --flake .#voyager
# Alternative with nh
nh darwin build -H voyager .
nh darwin switch -H voyager .
# NixOS systems
nixos-rebuild build --flake .#kepler
sudo nixos-rebuild switch --flake .#kepler
# Alternative with nh
nh os build -H kepler .
nh os switch -H kepler .Secrets are stored encrypted in secrets/secrets.yaml and automatically decrypted using sops-nix.
Key is stored in ~/.config/sops/age.key, make sure that exists.
Edit secrets
sops secrets/secrets.yamldotfiles/ holds config files home-manager deliberately doesn't own the contents of, because the apps (Zed, Claude Code) rewrite them at runtime. They aren't evaluated by the flake, but they are linked into place with mkOutOfStoreSymlink, so the live path is the file in this repo and there's nothing to sync manually. See dotfiles/README.md for why each app is there.
Services are defined as native NixOS or docker services. Not everything has been migrated to native yet.
Kepler service definitions live under hosts/kepler/services/ (Docker services in hosts/kepler/services/docker/).
sudo systemctl status $servicename
sudo systemctl start $servicename
sudo systemctl stop $servicename
sudo systemctl restart $servicename
journalctl -u $servicename -f
journalctl -u $servicename --since "1 hour ago"Docker-based services run in detached mode docker-compose up -d, so their container logs are not captured by journalctl.
cd /etc/docker-compose/$servicename
sudo docker-compose logs -f
# or
sudo docker-compose logs --tail=50This is intentional to keep container logs separate and avoid interleaving multiple container outputs in journald.
Both kepler and cubesat use restic for backups to an SFTP server, driven by
the same shared tooling (lib/restic-backup.nix) so both hosts are operated
identically — same systemd unit names, same restore CLI.
# Run backup manually
sudo systemctl start restic-backup
# Run pre-upgrade backup (tagged for easy identification/rollback)
sudo systemctl start restic-backup-preupgrade
# Check backup status/logs
journalctl -u restic-backup -fThe backup-restore command is available on both hosts:
backup-restore list # List all snapshots
backup-restore files latest # Browse files in a snapshot
backup-restore restore latest # Restore to /var/restore/
backup-restore restore-db latest # Extract PostgreSQL dumps (kepler only)
backup-restore verify # Non-destructively verify repo integrity + restorability
backup-restore shell # Interactive restic shell- Kepler backs up all services (native NixOS + Docker) daily at 4 AM.
- Cubesat backs up pangolin data (
/var/lib/pangolin) and uptime-kuma daily at 3 AM.
Use the deploy script to automatically create a pre-upgrade backup before deploying:
./scripts/deploy-with-backup.sh cubesatThis creates a tagged snapshot that can be used for rollback if needed.
backup-restore verify runs a full round-trip check without touching any
live data: it runs restic check against the repo, restores the latest
snapshot into a throwaway scratch directory (deleted automatically
afterward), and — on kepler — loads each PostgreSQL dump into a throwaway
database that's dropped immediately after a sanity check. Run it any time you
want confidence backups are actually restorable, on either host, the same way.
backup-restore list- find the snapshot you wantbackup-restore restore <snapshot-id>- extract to/var/restore/- Stop the relevant service:
sudo systemctl stop <service> - Copy files from restore dir to original location
- For PostgreSQL databases (kepler only):
sudo -u postgres psql <dbname> < /path/to/dump.sql - Start the service:
sudo systemctl start <service>
Backup failures are reported via healthchecks.io. Configure <host>_backup/healthcheck_url in sops secrets to enable.