NOTE: Code moved to https://git.firecat53.me/firecat53/nixos. Issues and PRs still accepted here for now. Github repo maintained as a read-only mirror.
- Laptop
laptop - Homeserver
homeserver - Backup server
backup - VPS cloud server
vps - Office secondary/spare desktop
office - Examples:
- Flake install w/ home-manager and sops.
- Encrypted or unencrypted
base-btrfsorbase-zfs
- Encrypted or unencrypted
- Bare minimum flake install for testing.
base-minimal
- Flake install w/ home-manager and sops.
Sops-nix secrets live in a private repository nixos-secrets. My directory
structure is:
~/nixos
~/nixos/nixos/
~/nixos/nixos-secrets
~/nixos/nix-neovim
pkgs/ contains derivations for small one-off apps maintained alongside this
repo. Each lives in pkgs/<name>/ with its own default.nix, and is wired up
through pkgs/default.nix ({ pkgs }: { ... = pkgs.callPackage ./<name> {}; }).
Service modules consume them with
localPkgs = import ../../../pkgs { inherit pkgs; }.
today— minimal Flask webapp for quick diary, workout, and book entries into the wiki. Deployed onhomeserverviahosts/homeserver/services/today.nixattoday.lan.firecat53.net.
Publicly-exposed services are driven by a single registry,
hosts/vps/services/registry.nix. The VPS reverse proxy (proxy-me.nix),
Authelia 2FA rules (authelia.nix), and Uptime-Kuma host resolution
(uptime-kuma.nix) are all derived from it.
Each entry's fields:
| field | where | meaning |
|---|---|---|
lan |
remote | homeserver .lan backend the VPS proxies to over wireguard |
port |
local | localhost port the VPS-local app binds to |
auth |
both | true gates the service behind Authelia 2FA from the internet |
passHost |
remote | optional; forward the real *.firecat53.me Host to the backend (see below) |
rules |
both | optional; per-service Authelia access_control rules (see below) |
Service running on homeserver:
- Create
hosts/homeserver/services/<name>.nixdefining the app and its own Traefik routerrule = "Host(\.lan.firecat53.net`)", and add it tohosts/homeserver/services/default.nix`. - Add one entry to the
remoteset inhosts/vps/services/registry.nix:<sub> = { lan = "<name>.lan.firecat53.net"; auth = <true|false>; };
auth = truegates the service behind Authelia 2FA when reached from the internet via<sub>.firecat53.me. - Rebuild
homeserver, thenvps. The<sub>.firecat53.merouter, the Authelia rule (ifauth = true), and Uptime-Kuma resolution appear automatically.
Services with their own HTTP basic auth (the homeserver auth
middleware — e.g. gollum, today, syncthing, transmission): the auth model is
basicAuth on the LAN, Authelia on the internet — not both. The VPS proxies
*.firecat53.me traffic in from 10.200.200.5, so add a second, companion
router on the homeserver that matches that source IP and omits the auth
middleware, letting Authelia-2FA'd requests through without a second prompt:
services.traefik.dynamicConfigOptions.http.routers.<name>-noauth = {
rule = "Host(\`<name>.lan.firecat53.net\`) && ClientIP(\`10.200.200.5\`)";
service = "<name>";
priority = 100; # beat the plain Host() router
middlewares = [ "headers" ]; # no "auth"
entrypoints = [ "websecure" ];
tls.certResolver = "le";
};LAN/wireguard clients keep hitting the plain Host() router and still get
basicAuth. This relies on the homeserver Traefik not trusting forwarded
headers (so ClientIP is the real TCP source). Services without basicAuth
(app-level login, or auth = false) need no companion router.
Exception — apps that build absolute redirects/URLs from the Host header
(e.g. gollum, sonarr): the ClientIP trick won't work, because the backend
would see the .lan host and redirect clients there (broken off-LAN). Instead
set passHost = true on its registry.nix entry so the VPS forwards the real
<sub>.firecat53.me host, and give it a companion router keyed on that host
(no ClientIP needed — only the VPS ever sends that host):
services.traefik.dynamicConfigOptions.http.routers.<name>-me = {
rule = "Host(\`<name>.firecat53.me\`)";
service = "<name>";
middlewares = [ "headers" ]; # no "auth"
entrypoints = [ "websecure" ];
tls = { }; # no certResolver: TLS uses the .lan SNI cert from the router above
};Service running locally on the VPS:
- Create
hosts/vps/services/<name>.nix(+ add it toservices/default.nix), binding the app to a localhost port. - Add one entry to the
localset inregistry.nix:<sub> = { port = <port>; auth = <true|false>; };
- Rebuild
vps.
Removing a service: delete its registry.nix entry (and the service file +
its default.nix import). The proxy router, auth rule, and monitor resolution
all disappear with it. Remove OIDC from authelia.nix if necessary.
The .lan backend name necessarily appears in two places — the homeserver
service file (its own router) and the VPS registry lan = value — because the
VPS must name the backend it proxies to across the wireguard tunnel. This
cross-host duplication is intentional.
Access Control rules: auth = true entries get a blanket two_factor rule.
To override that for specific paths (e.g. keep some public), add a rules list
to the entry — Authelia access_control rules evaluated before the blanket
rule, first match wins. Omit each rule's domain; authelia.nix derives it
from the attr name. For example, Microbin uses this to keep paste viewing public
but require authentication for submitting/delting pastes.
OIDC (apps that log in through Authelia rather than forward-auth, e.g.
immich, audiobookshelf): the client is defined in
identity_providers.oidc.clients in authelia.nix (hashed client_secret +
redirect_uris), and the app stores the plaintext secret in its own sops
secret. This pair is cross-host (Authelia on the VPS, app on the homeserver), so
it is not registry-derived. See the secret-generation commands at the top of
authelia.nix.
To add a host:
- Add it to
flake.nix(mkSystem). - Set its wireguard address in the host's
configuration.nix(networks."wg0".address). - Add its wireguard/LAN IPs to
networking.hostsinhosts/modules/desktops/networking.nix. - Add sops keys (see the install/post-install sections below).
- Install per General Install Procedures.
- Generate hostId (for ZFS systems):
head -c4 /dev/urandom | od -A none -t x4 - Hetzner VMs apparently require grub instead of systemd-boot (as of 2025-08)
- Available options: a. isVirtual (bool) - set for virtual hardware (VPS or VM). Default false. b. latestZFSKernel (bool) - set to use latest available ZFS compatible kernel. Default false.
Installing using nixos-anywhere
- Create new (Ubuntu is fine) cloud server. Add one of the public keys. Adjust DNS 'A' records if needed.
- SSH into the new box and update the disk device name(s) and partition layout (if needed) in disko-config.nix.
nix run github:nix-community/nixos-anywhere -- --generate-hardware-config nixos-generate-config ./hosts/<host>/hardware-configuration.nix --flake .#<host> --target-host root@<ip or domain>- If problems arise, add
--no-rebootto the above command so you can troubleshoot the new install. - [[#post-install]]
- Boot installer.
- Mount flash drive DATA
- Install:
mkdir ./mnt
sudo mount /dev/disk/by-label/DATA /home/nixos/mnt
rsync -av mnt/nixos .
cat /home/nixos/mnt/dotfiles/ssh-scotty/.ssh/id_ed25519.pub | sudo tee /root/.ssh/authorized_keys
# OR sudo passwd rootLogin via ssh from another machine (e.g. ssh root@192.168.200.103)
nix-shell -p git
mount -o remount,size=8G /run/user/0 ## This is to prevent out of space error during build
# Update device(s) in ~/mnt/nixos/nixos/hosts/<host>/disko-config.nix
nix --experimental-features "nix-command flakes" run github:nix-community/disko -- --mode disko /home/nixos/mnt/nixos/nixos/hosts/<host>/disko-config.nix
nixos-generate-config --no-filesystems --show-hardware-config --root /mnt --dir /home/nixos/mnt/nixos/nixos/hosts/<host>/
nixos-install --flake /home/nixos/mnt/nixos/nixos#<host>
cp -a /home/nixos/mnt/nixos /mnt/home/firecat53/ && chown -R 20000:100 /home/mnt/firecat53/nixos
umount /mnt/boot
umount /mnt
zfs export rpool
systemctl reboot- [[#post-install]]
Each desktop/laptop host gets its own SSH keypair — private halves never leave
the box, only pubkeys land in hosts/modules/common/ssh-keys.nix. Sequence
matters because the host can't reach itself via key auth until its pubkey is
authorized elsewhere.
- On the new/rebuilt host, generate the device key as
firecat53:
ssh-keygen -t ed25519 -C "firecat53@<hostname>" -f ~/.ssh/id_ed25519
wl-copy < ~/.ssh/id_ed25519.pub- On a working host with repo access, paste the pubkey into
hosts/modules/common/ssh-keys.nixunder the matchingdevices.<host>attribute. Commit and push. - Add the device pubkey to:
a. GitHub / forgejo account (web UI) — needed for git operations
b. HomeAssistant
~/.ssh/authorized_keysfor therootuser c. Any other external service the host needs to reach - Rebuild every host that should authorize this device:
- (Desktops/laptops using the autossh tunnel only) The passphraseless
autossh private key is shared across all tunnel clients and lives in
sops as
autossh-key. Add it to the host's sops file (the matching pubkey is already inssh-keys.nixasautossh, authorized on homeserver). To rotate, generate one new keypair, updateautosshinssh-keys.nix, and re-encryptautossh-keyinto every desktop sops file.
- new host Change
firecat53user and root (only for local machine) passwords. - new host Generate SSH keys per SSH key generation above.
- existing host Sync ~/nixos/ directory to new machine (including nixos configs and secrets)
- existing host Update sops key after reinstall. Commit and sync then rebuild.
nix shell nixpkgs#ssh-to-age nixpkgs#sops
ssh-keyscan <hostname> | ssh-to-age
# Set `&<hostname> age.....` in nixos-secrets/.sops.yaml
sops updatekeys nixos-secrets/<hostname>/secrets.yml
sops updatekeys nixos-secrets/common/secrets.yml
git add .sops.yaml <homename>/ && git commit -m 'Update sops keys'- existing host
nix flake updateand rebuild flake on target machine after sops key is updated. - new host
sudo nmcli connection import type wireguard file /etc/wireguard/wg0.conffor networkmanager. - Update syncthing device ID's if necessary. Re-add servers on phones and wife's laptop if needed.
- existing host
echo nixos/flake.lock > ~/nixos/.stignore(keep flake.lock from syncing)
- Copy/rename desired exmaple directory to hosts/xxxxx.
- Update CHANGEME items (disk device id, disk encryption, etc).
- Update configuration as desired.
a. If using base-btrfs with encryption, rename
disko-config-luks.nixtodisko-config.nix - Add new host to flake.nix.
- Sops-nix (if needed):
a. Add any sops-nix keys to nixos-secrets/xxxx/secrets.yml
b. Add new host to nixos-secrets/.sops.yml
c.
sops updatekeyshappens after install d. Update flake inputs - Install using nixos-anywhere
- [[#Installing locally on a new machine using the ISO installer]]
sudo smbpass -a jamiassh-keygen -f /etc/ssh/backup && chown backup: /etc/ssh/backup. ChangebackupPullto the public key inssh-keys.nixand rebuild all servers.sudo -i -u backup ssh -i /etc/ssh/backup <backup source hostname(s)>and accept fingerprint
- [[#Installing locally on a new machine using the ISO installer]]
- Login to Vaultwarden
- Login to Firefox Sync a. Extensions - ClearURLs, floccus, Gnome Shell integration, Proxy SwitchyOmega 3, Stylus, uBlock Origin, User-Agent switcher and Manager, Vimium
- Open Syncthing on this machine and other machines. Ensure syncing is setup.
- Stow (dotfiles)
cd home/firecat53/docs/family/scott/src/dotfiles
stow -t /home/firecat53/ --dotfiles stow/
stow gomuks music passwords python ssh-scottyThis directory contains disko configuration for homeserver's two-NVMe-drive mirrored ZFS setup with systemd-boot.
Both NVMe drives have identical partition layouts:
| Part | Size | Purpose |
|---|---|---|
| p1 | 1G | EF00 ESP (vfat) - /boot on nvme0 |
| p2 | 4G | Unused (legacy bpool placeholder) |
| p3 | ~1.8T | rpool (ZFS mirror) |
| p4 | 8G | Encrypted swap |
- rpool: Mirrored across both NVMe drives
- ESP: Only the first drive's ESP is mounted at
/boot(systemd-boot) - datapool: Separate SATA drives (not managed by disko)
WARNING: Do NOT run disko --mode disko on an existing system. It would
reformat the drives. The disko config is used only as a NixOS module for
fileSystems generation, and as a reference for future fresh installs.
-
Boot into NixOS installer
-
Clone your configuration
git clone <your-repo-url> /tmp/nixos-config
cd /tmp/nixos-config- Review and adjust disko-config.nix
Check these settings in hosts/homeserver/disko-config.nix:
- Disk devices: Update device paths to match your drives
- Partition sizes: Adjust if needed (swap=8G, rpool uses remaining space)
- Pool/dataset options: Modify compression, reservation, etc. as desired
- Run disko to partition and format
sudo nix run github:nix-community/disko -- --mode disko /tmp/nixos-config/hosts/homeserver/disko-config.nixThis will:
- Partition both NVMe drives
- Create the mirrored rpool ZFS pool and all datasets
- Format the ESP partition
- Set up encrypted swap on both drives
Note: This does NOT touch the SATA drives (datapool). Import datapool separately after install.
- Install NixOS
Disko automatically mounts everything to /mnt.
sudo nixos-install --flake /tmp/nixos-config#homeserver- Reboot
reboot- Post-install
sudo zpool import -f datapool1-4. Follow Scenario A steps 1-4
Run disko to partition, create pools, and mount everything. This creates empty datasets.
- Receive ZFS data into the new pools
Before installing NixOS, populate the datasets with your data:
# Import the old/backup pool with an alternate name
sudo zpool import -R /tmp/oldpool oldrpool
# Recursive send of all data datasets
sudo zfs snapshot -r oldrpool/data@migrate
sudo zfs destroy -r rpool/data
sudo zfs send -R oldrpool/data@migrate | sudo zfs recv rpool/data
# Fix mountpoints to use legacy (disko expects legacy mounts)
sudo zfs set mountpoint=legacy rpool/data/home
sudo zfs set mountpoint=legacy rpool/data/podman_volumes
# ... etc for each datasetNote: You generally don't need to migrate system datasets (rpool/nixos/*)
since NixOS will rebuild those during install. Focus on the rpool/data/*
datasets. datapool lives on separate SATA drives - just import it directly.
After migrating, re-mount everything:
sudo umount -R /mnt
sudo nix run github:nix-community/disko -- --mode mount /tmp/nixos-config/hosts/homeserver/disko-config.nix- Clean up and install
sudo zpool export oldrpool
sudo nixos-install --flake /tmp/nixos-config#homeserver
reboot- Boot: enter the firmware boot menu and select the second NVMe drive. It boots via EFI/BOOT/BOOTX64.EFI (the removable-media fallback).
- Replace the dead drive in the ZFS mirror:
sudo zpool replace rpool <old-nvme0-part3> /dev/disk/by-id/<new>-part3
- Recreate the ESP on the replacement drive and let the next rebuild resync:
sudo mkfs.vfat -F32 -n ESP /dev/disk/by-id/<new>-part1sudo nixos-rebuild boot --flake .#homeserver# repopulates /boot