Nick Hiebert's personal repository.
  • Elm 63.6%
  • Haskell 25.2%
  • Nix 7.5%
  • TypeScript 1.6%
  • Kotlin 0.5%
  • Other 1.4%
Find a file
2026-10-08 15:25:39 -05:00
.forgejo/workflows fix(forgejo): added enaychi to the forgejo runner 2026-09-24 01:24:33 -05:00
lib feat(dotfiles): updated secrets and prod infra 2026-09-28 13:26:42 -05:00
modules chore(homelab): shelve projectsite 2026-10-08 13:39:40 -05:00
packages style(enaychi): custom food styling conformity 2026-10-08 15:25:39 -05:00
parts fix(forgejo): added enaychi to the forgejo runner 2026-09-24 01:24:33 -05:00
profiles/user0 chore(global): init 2026-09-21 16:32:38 -05:00
secrets fix(enaychi): Google Play Store compliance 2026-09-30 14:33:26 -05:00
systems fix(peertube): database perms 2026-09-24 02:35:56 -05:00
templates chore(global): init 2026-09-21 16:32:38 -05:00
.envrc chore(global): init 2026-09-21 16:32:38 -05:00
.gitignore feat(nutridex): nutridex separation and renaming 2026-09-23 01:02:11 -05:00
.sops.yaml chore(global): init 2026-09-21 16:32:38 -05:00
flake.lock chore(flake): updated 2026-09-24 12:09:11 -05:00
flake.nix test(uprootnutrition): peertube 2026-09-22 02:09:44 -05:00
LICENSE docs(global): added license 2026-10-05 22:08:24 -05:00
README.md feat(nutridex): nutridex separation and renaming 2026-09-23 01:02:11 -05:00
schema.mermaid chore(global): init 2026-09-21 16:32:38 -05:00

Nick's NixOS Dotfiles

A single flake that builds every machine I own: two desktops, two laptops, a server, a NAS, an installer ISO, the microVM guests that run my self-hosted services, and the first-party applications those guests serve.

It is a flake-parts repository. Host configurations, Home Manager profiles, NixOS modules, Haskell and Elm packages, deployment nodes, dev shells and formatter hooks are all outputs of the same evaluation, so a change to a shared record propagates to every consumer at build time instead of drifting.

  • Structure: flake-parts, with import-tree discovering modules instead of hand-written aggregator files
  • Deployment: deploy-rs over SSH as root, key only
  • Isolation: microvm.nix guests, one service per guest, declared through a local homelab.microvms abstraction
  • Secrets: sops-nix with age
  • Nix implementation: Lix
  • CI: Forgejo Actions, self-hosted runner, five hosts built per run

Hosts

Naming follows a celestial convention: desktops are planets, laptops are moons, servers are dwarf planets.

Host Role Notable
mars Main desktop Hyprland, Niri and Plasma, ollama, local PostgreSQL, dev instances of both web apps
deimos Laptop Hyprland, full GUI profile
phobos Secondary laptop Minimal, CLI only, microVM host
ceres Primary server Caddy, ACME, Forgejo plus runner, Jellyfin, Mastodon, Minecraft, torrenting stack, the Grafana observability stack, production web apps
eris NAS and secondary server NFS exports, Vaultwarden, PhotoPrism, WireGuard, Restic
iso Installer image Bootstrapping only

Each host is systems/<name>/, which is a two-line entry point plus a config/ directory of boot, filesystem, hardware, networking, graphics, sops and WireGuard fragments that import-tree picks up automatically.

Layout

flake.nix          Inputs, nixosConfigurations, templates
lib/               Config records, helper functions, deploy-rs nodes
modules/nixos/     System modules, grouped into named bundles
modules/home/      Home Manager modules, one directory per program
profiles/user0/    The user account, its home, dotfiles and scripts
systems/           Per-host hardware and machine-specific config
packages/          First-party applications and tools
parts/             Dev shell, pre-commit hooks, treefmt, flake checks
secrets/           sops-encrypted secrets
templates/         `nix flake init -t` starters for new projects

lib/

Three things live here.

lib/options/ is the heart of the repo: a typed schema for the facts that more than one module needs to agree on. Four record trees are declared as NixOS options and populated from data files:

  • machines.devices per host: hostnames, LAN addresses, WireGuard keys and IPs, boot and storage mount points with their mount options
  • services.instances per service: domains, subdomains, ports, ACME cert paths, state and cache paths, and the microVM interface details (MAC, tap discriminator, guest IP, SSH port)
  • people.users: names, labels, SSH keys, email addresses, group
  • aesthetics.themes: the active palette, fonts and sizes, cursor, window manager gaps, borders and rounding

Because these are options rather than plain attribute sets, a typo in a service record fails evaluation instead of silently producing an empty string somewhere in a Caddy vhost. The raw values feeding them live in lib/records.nix.

lib/helpers/ exposes mkLinuxSystem and mkHome, thin wrappers over nixosSystem and homeManagerConfiguration that fix allowUnfree and thread inputs, self and the flake-level config through as specialArgs. That last part is why every module in the tree can write flake.config.services.instances.jellyfin and get the same record.

lib/deploy/ maps the same host IP records into deploy-rs nodes, with activation and confirmation timeouts raised well past the defaults because microvm@ units are Type=notify and a host with several guests takes minutes to finish activating.

modules/nixos/

Modules are discovered by directory, then composed into named bundles in modules/nixos/default.nix:

  • core goes on every host: accounts, environment, fonts, Home Manager wiring, locale, nh, nix settings, rsync, SSH, sudo, system stats
  • mantle adds sops and the X server
  • crust adds desktop hardware and program modules
  • one bundle per host name, listing exactly what that machine runs

Under homelab/ sit the service definitions:

  • guests/ is one directory per service, each split into a shared config/ that describes the guest itself and a per-host wrapper (jellyfinCeres, vaultwardenEris, and so on) that places it on a machine. Roughly two dozen guests: Forgejo, Jellyfin, Mastodon, Minecraft, Vaultwarden, PhotoPrism, Syncthing, OpenCloud, Firefly III, Open WebUI, the torrent stack, Grafana, Loki, Mimir, Tempo, and the dev and prod instances of the web apps.
  • orphans/ holds services that run on the host rather than in a guest: ComfyUI, PostgreSQL, Glance, SearXNG, PeerTube. Some are wired into a host, some are parked.
  • caddy/ is the reverse proxy, with one config directory per vhost. It refuses to start if it would serve a domain whose ACME certificate has never actually been issued, since the placeholder in that case is a self-signed minica cert.
  • restic/, nfs/, alloyCeres/alloyEris, nasDirs, forgejoRunner and userConfigs/ cover backups, exports, telemetry shipping, NAS directory layout, CI and shared service accounts.

microvm/ is the local guest abstraction. homelab.microvms.<name> takes an IP, a MAC, resources and a guest module, and emits the microvm.vms entry, the systemd-networkd unit matched by MAC, the tap interface, the virtiofs shares for state and secrets, and the tmpfiles rules behind them. Tap names are derived from the attribute name, so two guests cannot collide, and names past the kernel's 15-character interface limit fold to a hash rather than truncating into a collision. An assertion catches duplicates at evaluation time.

The same module ships microvm-sync, which restarts exactly the guests whose booted and current symlinks have diverged. A host rebuild moves those symlinks but does not restart the VMs, so without it a memory or CPU change looks applied while the running guest still holds its old allocation.

modules/home/

Home Manager modules, one directory per program, grouped as cli/ (dev tooling, file utilities, shell, general utilities) and gui/ (browsers, editors, crypto, terminals, gaming, media, messaging, file sharing, tools). modules/home/default.nix composes them into one profile per host and user pair, keyed off the same people and machine records, so mars gets the full desktop set and ceres gets cli only.

The shell is Nushell with Starship, the editor set is Helix and Zed, and theming is driven from aesthetics.themes so palettes stay consistent across the terminal, window manager and applications.

profiles/user0/

The user account itself: declarative users with mutableUsers = false and a sops-provided password hash, group membership, home directory structure via systemd.tmpfiles, helper scripts, and a generated justfile giving every machine local and remote rebuild recipes, SSH shortcuts and a vms-<host> recipe that calls microvm-sync remotely.

packages/

First-party code, exposed as flake packages with a matching dev shell each:

Package What it is
uproot-backend upRootNutrition backend, Haskell
uproot-frontend upRootNutrition frontend, Elm, built with mkElmDerivation
uproot-android Capacitor and Gradle orchestrator for the upRootNutrition Android app, nix run .#build-android -- [release|debug|compile-only]
haskeww Haskell library and scripts backing the status bar: audio, brightness, battery, GPU, time, weather, workspaces
btrfs btrfs-cleaner, snapshot pruning
resume Typst résumé, two variants, built to PDF

The Android packages pin their own nixpkgs instance with android_sdk.accept_license, since the repo-wide allowUnfree only covers NixOS and Home Manager evaluation.

Haskell packages use committed cabal2nix output rather than callCabal2nix, so nothing in the tree needs import-from-derivation. Regenerate the sibling <pkg>.nix when .cabal dependencies change.

templates/

nix flake init -t starters, each with a flake, a dev shell, a .envrc and a formatter config:

nix flake init -t github:<this-repo>#haskell   # Haskell with cabal and HLS
nix flake init -t github:<this-repo>#elm       # Elm Land frontend with a Haskell backend
nix flake init -t github:<this-repo>#typst     # Typst documents

Development

direnv allow drops you into the default shell, which carries sops, age, ssh-to-age, deploy-rs, nixfmt, nil, just and the rest of the day-to-day tooling, and installs the pre-commit hooks on entry.

Two formatting layers, deliberately kept in agreement:

  • pre-commit runs nixfmt on staged Nix files and commitizen on the commit message, so commits follow Conventional Commits.
  • treefmt (nix fmt) covers the whole tree: nixfmt, deadnix, statix, ormolu, hlint, cabal-fmt, elm-format, rustfmt, taplo, typstyle, yamlfmt, mdformat. Its nix flake check gate is off, because the tree is not yet treefmt-clean across every language. nixfmt width is pinned to 100 in both layers so the two never fight.

Per-project shells are available for each package, for example nix develop .#uproot-backend or nix develop .#uproot-frontend.

Secrets

Everything sensitive lives in secrets/secrets.yaml, encrypted with age through sops-nix and decrypted to /run/secrets at activation. .sops.yaml holds the recipient list. Guests receive secrets through a read-only virtiofs share from the host rather than holding their own keys.

Note that evaluating this flake requires SSH access to a private forge, because one input is an out-of-tree project flake. Both the workstation and the CI runner get the host key from programs.ssh.knownHosts in modules/nixos/core/ssh.

Building and deploying

# Local host
nixos-rebuild switch --sudo --flake .#mars

# Remote host, over deploy-rs
deploy .#ceres

# Remote host, over nixos-rebuild
nixos-rebuild switch --flake .#ceres --target-host 192.168.50.240 --sudo --ask-sudo-password

# Restart only the guests whose configuration actually changed
ssh -t ceres sudo microvm-sync

# Build without switching
nix build .#nixosConfigurations.ceres.config.system.build.toplevel

# Evaluate everything
nix flake check

The generated justfile on each machine wraps most of these: just rebuild, just rebuild-<host>, just <host> for SSH, just vms-<host> for the guest sync.

CI

.forgejo/workflows/ci.yml runs on a self-hosted runner. A cheap nix flake check --no-build gates an expensive matrix that builds all five host toplevels, so a flake that does not even evaluate never burns five serial builds. Runs supersede each other per ref, since Nix keeps whatever a cancelled run already realised.

The main branch is ignored on push by design: this repo is the development fork and pushes to main are constant, while upstream only ever sees already-tested merges. The real gate is the pull request.

Credits

Not everything here is original. The material below belongs to other people, is used with attribution, and carries its own terms rather than the repository's.

What Where Whose
haskeww, the EWW status bar library and scripts packages/haskeww/ Originates from the AskYourself project, AGPL-3.0-or-later, same as this repository
Many of the SVG icons in the Elm frontends src/Config/Style/Icons/ in each Elm package, and templates/elm/ Font Awesome Free, Fonticons Inc., CC BY 4.0
Notification sounds packages/upRootNutrition/uproot-frontend/static/alerts/ akx/Notifications, Aarni Koskela and contributors, CC BY 4.0
OBS Studio themes modules/home/gui/apps/media/video/obsStudio/themes/*.qss Philippe Groarke, GPL-2.0-or-later
OBS Studio and Thunderbird themes .../obsStudio/themes/Catppuccin/, .../thunderbird/themes/*.xpi Catppuccin contributors, MIT

Acknowledgements

The upRootNutrition branding, including Enaychi, and the compatIQ branding are the work of ardlak on Discord (user ID 91007151513825280).

Licence

AGPL-3.0-or-later for everything original to this repository. AGPL over GPL deliberately: both web applications are hosted services, and §13 is what keeps a modified hosted copy from being a private fork. The GPL-2.0-or-later OBS themes credited above are why the project licence has to stay GPL-compatible.

Two paths are present in the tree but are not granted under the AGPL, because they are not mine to relicense. Strip them before redistributing, or satisfy yourself independently that you hold the rights:

Path What it is
modules/home/gui/apps/browsers/firefox/config/search/config/icons/ Search-engine logos and trademarks, reproduced to identify the engines they belong to — the same nominative use a browser's own search settings make of them
packages/resume/ Personal résumé content and contact details

"upRootNutrition", as well as its logos and wordmarks, are not licensed by the AGPL grant either. The licence covers the software; it does not grant permission to run a service under these names or to present a fork as the original.