Personal Nix Config
  • Nix 92.6%
  • Just 7.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Hristo Djenkov e32269bc5f
Some checks failed
check / check (push) Has been cancelled
idfk
2026-09-05 15:27:01 +03:00
.forgejo/workflows idfk 2026-07-30 01:24:49 +03:00
modules idfk 2026-09-05 15:27:01 +03:00
.envrc cleanup + power-user additions 2026-06-09 15:41:57 +03:00
.gitignore cleanup + power-user additions 2026-06-09 15:41:57 +03:00
flake.lock idfk 2026-09-05 15:27:01 +03:00
flake.nix idfk 2026-09-05 15:27:01 +03:00
justfile idfk 2026-09-05 15:27:01 +03:00
LICENSE add license 2026-02-03 23:53:03 +01:00
README.md idfk 2026-09-05 15:27:01 +03:00

nix-config

Personal Nix configuration for macOS (nix-darwin) and NixOS machines.

Structure

modules/
├── machines/
│   ├── nixos/          # Explicit NixOS host definitions
│   │   ├── _common/    # shared NixOS boilerplate
│   │   ├── htpc/
│   │   ├── hass/
│   │   ├── vps/
│   │   └── installer/
│   └── darwin/         # nix-darwin host definitions
│       ├── _common/    # shared darwin boilerplate
│       └── lambda/
├── homelab/            # homelab.services.* option modules (vps + hass services)
├── misc/               # reusable cross-host modules (tailscale, …)
├── users/hdjenkov/     # user account + home-manager entry
├── dots/               # home-manager dotfile modules
└── devshell.nix        # flake-parts perSystem: treefmt + dev shell
justfile                # deploy/check/update shortcuts

Inputs

Every host tracks the current NixOS release, nixos-26.05. nix-darwin, Home Manager and nixvim are pinned to the matching branch, so lambda, htpc, hass and vps share one nixpkgs and one set of module semantics — there is no separate stable/unstable split to keep in sync.

nixpkgs-unstable is the one exception, consumed only by modules/dots/vscode. See "App configuration that is not a store symlink".

Machines

Host Type Description
lambda nix-darwin (aarch64) MacBook Pro workstation
htpc NixOS (x86_64) Home theater PC
hass NixOS (x86_64) Home automation + print/scan server (LAN only)
vps NixOS (aarch64) VPS — self-hosted services

Services

htpc

Service Description
Kodi Media center with Jellycon addon

hass

Service Description
Home Assistant Home automation, container (pinned tag, host network)
CUPS LAN print server for the Brother DCP-1510E (brlaser), AirPrint via Avahi
scanservjs Browser scanning UI, driven by the host's SANE/brscan4 over saned
Glances Host metrics over a loopback-only REST API, scraped by Home Assistant
cage Wayland kiosk on the attached monitor: Chromium full-screen on the HA dashboard

vps

Service Description
Caddy Reverse proxy with automatic TLS (Cloudflare DNS)
frp Tunnel server (frps) for home-hosted services
Forgejo Self-hosted git
Forgejo Runner CI runner (docker-in-docker)
Headscale Self-hosted Tailscale control server
Headplane Headscale web UI
Pocket ID OIDC/SSO provider
Uptime Kuma Uptime monitoring
FMBot Last.fm Discord bot (via fmatrix)
Epic Games Free Games Auto-claim free Epic Games
Website Static site

Home-hosted services tunneled via frp (frpc runs on Unraid):

Service Description
Immich Photo library
Nextcloud File storage

Secrets

Secrets are managed with agenix and stored in a private repository. Each machine decrypts secrets using its SSH host key (NixOS) or personal SSH key (macOS).

Deploy

Use just --list to see all available targets. Common workflows:

just update                 # preview input updates without changing flake.lock
just update-one nix-darwin  # update one input, then validate lambda
just update-apply           # update everything, format, evaluate and build lambda
just preflight-lambda       # evaluate all systems and build lambda
just diff-lambda            # compare the next lambda closure with the live one
just deploy-lambda          # preflight, show the nh diff and activate
just generations-lambda     # list generations available for rollback
just rollback-lambda        # activate the previous generation
just deploy-vps             # remote build + switch on vps
just deploy-htpc            # remote build + switch on htpc
just deploy-hass            # remote build + switch on hass
just build-iso              # build installer ISO

Installation

macOS (lambda)

Install Nix via Determinate Systems:

curl -fsSL https://install.determinate.systems/nix | sh -s -- install

Clone the repo:

git clone https://<git-host>/hdjenkov/nix-config.git ~/git/nix-config

Clone the private secrets repo:

git clone git@<git-host>:hdjenkov/nix-private.git ~/git/nix-private

Apply the configuration:

sudo darwin-rebuild switch --flake ~/git/nix-config#lambda --override-input secrets ~/git/nix-private

NixOS (htpc)

Boot from the installer ISO, then from your workstation copy your SSH key to the target:

export NIXOS_HOST=192.168.x.x
ssh-copy-id -i ~/.ssh/personal hdjenkov@$NIXOS_HOST

SSH in with agent forwarding:

ssh -A hdjenkov@$NIXOS_HOST

Enable flakes:

mkdir -p ~/.config/nix
echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf

Partition and format using disko:

nix --experimental-features "nix-command flakes" run github:nix-community/disko \
  -- -m destroy,format,mount \
  /path/to/nix-config/modules/machines/nixos/htpc/disk-config.nix

Install git and clone the repo:

nix-env -f '<nixpkgs>' -iA git
mkdir -p /mnt/home/hdjenkov/git
git clone https://<git-host>/hdjenkov/nix-config.git /mnt/home/hdjenkov/git/nix-config

Install:

nixos-install --root /mnt --no-root-passwd \
  --flake git+file:///mnt/home/hdjenkov/git/nix-config#htpc

Reboot, then SSH in with agent forwarding and add the host to the secrets repo:

ssh -A hdjenkov@$NIXOS_HOST
cat /etc/ssh/ssh_host_ed25519_key.pub

Add the host key to secrets.nix in the private repo, re-encrypt, and push:

cd ~/git/nix-private
agenix -r
git add -A && git commit -m "add htpc host key" && git push

Pull and deploy:

git clone https://<git-host>/hdjenkov/nix-config.git ~/git/nix-config
git clone git@<git-host>:hdjenkov/nix-private.git ~/git/nix-private  # SSH agent forwarded
cd ~/git/nix-config && git pull
sudo --preserve-env=SSH_AUTH_SOCK nixos-rebuild switch --flake ~/git/nix-config#htpc --override-input secrets ~/git/nix-private

NixOS (hass)

Boot the official NixOS minimal ISO and work at the console (user nixos, no password). On Wi-Fi, connect first with nmtui, or:

sudo systemctl start wpa_supplicant
wpa_cli
> add_network
> set_network 0 ssid "SSID"
> set_network 0 psk "PASSWORD"
> enable_network 0
> quit

Install git and clone the repo:

nix-env -f '<nixpkgs>' -iA git
git clone https://<git-host>/hdjenkov/nix-config.git /tmp/nix-config

Partition and format using disko:

lsblk    # confirm the target disk
nix-shell -p disko --run \
  "disko -m destroy,format,mount /tmp/nix-config/modules/machines/nixos/hass/disk-config.nix"

disk-config.nix targets /dev/nvme0n1 and wipes it unconditionally.

Use nix-shell here, not nix run github:nix-community/disko. The ISO store is a ~2GB tmpfs and the flake pulls its own nixpkgs (330MB unpacked) into it, which runs out of space on 4GB; nix-shell uses the channel already on the ISO. mount -o remount,size=3G /nix/.rw-store buys headroom if still tight.

Install. No --no-root-passwd here — set a root password, it is the only way back in if the box boots without network:

sudo nixos-install --root /mnt --flake /tmp/nix-config#hass -j 2 --cores 2

nixos-install builds with --store /mnt, so the closure lands on the NVMe, not the ISO's RAM overlay. It also rejects --override-input.

On Wi-Fi, copy the connection profile over before rebooting, or the installed system comes up with no network:

sudo cp /etc/NetworkManager/system-connections/*.nmconnection \
        /mnt/etc/NetworkManager/system-connections/
sudo chmod 600 /mnt/etc/NetworkManager/system-connections/*.nmconnection

Reboot, then SSH in and add the host key to the secrets repo:

ssh hdjenkov@hass.local
cat /etc/ssh/ssh_host_ed25519_key.pub

Add the host key to secrets.nix in the private repo, re-encrypt, and push:

cd ~/git/nix-private
agenix -r
git add -A && git commit -m "add hass host key" && git push

Then just deploy-hass from the workstation.

hashedUserPassword decrypts with the SSH host key, which does not exist until first boot, so console login for hdjenkov only works after this deploy. SSH key auth and passwordless sudo work immediately.

Manual steps outside Nix, once:

  • Printerhttp://hass:631 → Administration → Add Printer, pick the brlaser PPD. Writes /etc/cups/printers.conf. lpinfo -v gives the device URI if you later want hardware.printers.ensurePrinters.

  • Scanner — check scanimage -L. If brscan4 misses the device, register it with brsaneconfig4 -a. The scanservjs UI at http://hass:8080 only sees what the host sees, since it goes through saned rather than USB.

  • Home Assistant — onboard at http://hass:8123.

  • Kiosk login — the panel skips the login screen entirely, so no keyboard is ever plugged into it. Add to /var/lib/home-assistant/configuration.yaml and restart the container:

    homeassistant:
      auth_providers:
        # trusted_networks MUST come first. The frontend starts the flow for the
        # first provider in this list and offers the rest behind "log in with
        # other providers", so with homeassistant first the panel shows a
        # password form even though the bypass works.
        - type: trusted_networks
          trusted_networks:
            - 127.0.0.2/32
          allow_bypass_login: true
          # allow_bypass_login only skips the prompt when the address maps to
          # exactly one user; pinning the id keeps that true if another HA
          # account is ever added. Ids come from .storage/auth.
          trusted_users:
            127.0.0.2: <user_id>
        # Listing trusted_networks replaces the default provider list, so
        # homeassistant must be named explicitly or password login disappears
        # everywhere — phone included.
        - type: homeassistant
    

    Verify the provider is offered, and only to the kiosk:

    curl -s http://127.0.0.2:8123/auth/providers   # includes trusted_networks
    curl -s http://127.0.0.1:8123/auth/providers   # homeassistant only
    

    The kiosk browses http://127.0.0.2:8123. kiosk-source-address.service adds 127.0.0.2/32 to lo, which makes it the preferred source for that destination — without the /32 the only match is the kernel's local 127.0.0.0/8, whose preferred source is 127.0.0.1, and the kiosk would be indistinguishable from Caddy. Caddy proxies to 127.0.0.1, so LAN clients arriving through the vhost stay untrusted and still get a login screen.

    Confirm with a real socket, not ip route get — the latter answers with the local 127.0.0.0/8 route's prefsrc (127.0.0.1) and will look like a failure even when everything is correct:

    bash -c 'exec 3<>/dev/tcp/127.0.0.2/8123; ss -tn "dst 127.0.0.2"'
    

    The Local Address column is what HA sees. It must read 127.0.0.2.

    Do not swap 127.0.0.2 for 127.0.0.1 here. Caddy strips X-Forwarded-For (see modules/homelab/services/home-assistant/default.nix), so HA sees every proxied client as 127.0.0.1; trusting it would auto-log-in the whole network.

  • Glances integration — Settings → Devices & Services → Add Integration → Glances, once per host: 127.0.0.1 (this box), the Unraid box and the OpenWRT router, all on port 61208. Then build the dashboard from the resulting sensors.

  • Wi-Fi — the copied profile is mutable state; move the PSK into an agenix secret and networking.networkmanager.ensureProfiles to make it declarative.


NixOS (vps)

From your workstation, copy your SSH key to the VPS:

export NIXOS_HOST=<vps-ip>
ssh-copy-id -i ~/.ssh/personal root@$NIXOS_HOST

SSH in with agent forwarding:

ssh -A root@$NIXOS_HOST

Enable flakes:

mkdir -p ~/.config/nix
echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf

Partition and format using disko:

nix --experimental-features "nix-command flakes" run github:nix-community/disko \
  -- -m destroy,format,mount \
  /path/to/nix-config/modules/machines/nixos/vps/disk-config.nix

Install git and clone the repo:

nix-env -f '<nixpkgs>' -iA git
mkdir -p /mnt/home/hdjenkov/git
git clone https://<git-host>/hdjenkov/nix-config.git /mnt/home/hdjenkov/git/nix-config

Install:

nixos-install --root /mnt --no-root-passwd \
  --flake git+file:///mnt/home/hdjenkov/git/nix-config#vps

Reboot, then SSH in and add the host key to the secrets repo:

ssh hdjenkov@$NIXOS_HOST
cat /etc/ssh/ssh_host_ed25519_key.pub

Add the host key to secrets.nix in the private repo, re-encrypt, and push:

cd ~/git/nix-private
agenix -r
git add -A && git commit -m "add vps host key" && git push

Updating

Package ownership

  • nix-darwin owns macOS settings, services, fonts, and system integration.
  • Home Manager owns user CLI tools and their configuration.
  • Homebrew owns native macOS applications and Colima.
  • Project-specific language toolchains belong in project dev shells.

Three GUI apps rewrite their own config files, so Home Manager cannot symlink them read-only out of the store — the app either errors or silently reverts to defaults. They are handled as writable copies instead:

App Repo source Activation behaviour Import back
Karabiner-Elements modules/dots/karabiner/karabiner.json overwritten on every activation just karabiner-import
VS Code modules/dots/vscode/settings.json seeded only when absent just vscode-import

Karabiner is overwritten because its rules only change when you deliberately edit them — the repo is the source of truth. VS Code's settings.json is only seeded, because extensions write to it continuously (model pickers, tool approvals, view state) and an overwrite on every deploy would throw that away.

VS Code extensions are declarative, in modules/dots/vscode/default.nix. mutableExtensionsDir = true keeps ~/.vscode/extensions writable, so pinned extensions are symlinked in and anything not packaged in nixpkgs is installed from the marketplace and survives activation. Currently marketplace-only:

  • opentofu.vscode-opentofu

Both VS Code and its extensions come from the nixpkgs-unstable input. Extensions are not backported to the release branch, so the pins there run many versions behind the marketplace. The two have to move together: an extension built against VS Code 1.133 can declare an engine range that 26.05's 1.119 refuses to load. This is the only module in the repo that leaves the release branch.

The Nix copies are read-only, so VS Code cannot update them in place — a version bump means a just update-one nixpkgs-unstable. After a deploy, run just vscode-dedupe to drop marketplace copies of extensions Nix now pins; VS Code keeps the higher version of the two and a stale marketplace copy would silently win. just vscode-unmanaged lists every version-suffixed (marketplace-installed) extension, which is the set to review for promotion into the module.

Raycast

Raycast is not declaratively configurable. Extensions, quicklinks, snippets, aliases and per-command hotkeys live in an opaque store under ~/Library/Application Support/com.raycast.macos. Only the global hotkey and the hyper-key icon are defaults keys, and those are set in modules/machines/darwin/lambda/system.nix.

Everything else comes back through Raycast Settings → Cloud Sync, which must be enabled before wiping. The extensions currently installed, for a manual rebuild if sync is unavailable:

Spotify Player, Color Picker, GitLab, GIF Search, Coffee, Brew, Visual Studio Code, Tailscale, Kill Process

Darwin activation installs missing Homebrew declarations and uninstalls undeclared packages. It uses Homebrew's normal uninstall mode, not zap, so application preferences and support data are preserved. Activation does not update or upgrade installed packages; run just brew-maintenance explicitly when you want to do that. Run just brew-cleanup-preview to see what activation would remove before deploying.

Sikarugir is declared with per-cask trust because it comes from a third-party tap. The rest of that tap is not trusted.

System updates

lambda:

cd ~/git/nix-config && git pull
sudo darwin-rebuild switch --flake ~/git/nix-config#lambda --override-input secrets ~/git/nix-private

Or with just:

just preflight-lambda
just diff-lambda
just deploy-lambda

If the new generation causes a regression, just rollback-lambda activates the previous generation. just generations-lambda shows the retained history.

htpc (SSH in with -A, then):

cd ~/git/nix-config && git pull
sudo nixos-rebuild switch --flake ~/git/nix-config#htpc

After the first successful deploy, htpc preserves SSH_AUTH_SOCK through sudo automatically, so it can fetch the secrets repo directly without --override-input.

vps (built and deployed remotely from workstation):

cd ~/git/nix-config && git pull
just deploy-vps

Or manually:

nixos-rebuild switch --flake .#vps \
  --target-host hdjenkov@<vps-host> \
  --build-host hdjenkov@<vps-host> \
  --sudo --ask-sudo-password