Skip to content

About

Nix configuration for an entire fleet of macOS and NixOS hosts

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3,910 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nix

Here I store my notes regarding Nix as it pertains to this repository. This will include some findings and also projects I’m working on. I do have separate notes for Nix in my private notes repository, and I haven’t yet come up with a method of consolidating them.

This helps me close the many tabs I have open. This is very incomplete and I have many more tabs to go. I think I like this as a general “wut I do in Nix” scratch pad though.

Conventions for adding to this repo (including where new docs live and how they get linked from here) are in CONTRIBUTING.org.

README content

The purpose of this document is to provide a rough outline of how this repo is structured and where high level documents can be found. It doesn’t cover workarounds or specific subsystems. Anything else is leftovers from before this statement of content was made.

creating new hosts

create Nix host file

Look up the next element in the periodic table, and then create that under nix/hosts.

deploy tools

proton-deploy is vastly preferred for all deployments. It uses Nix’s copy-closure mechanism to transfer store paths to the remote host securely.

remote-deploy is a break-glass script for working around limitations of the copy-closure mechanism. Known cases where it may be necessary are documented in troubleshooting-methods.org. After using remote-deploy to recover, return to proton-deploy for subsequent deployments.

remote-deploy works by rsyncing the entire repo to ~~/proton-nix~ on the remote host. Sensitive files from the repo may be present there. By default, ~~/proton-nix~ is removed after deployment completes. Pass --no-cleanup to retain it.

remote-deploy is structurally incompatible with deploys driven from the private repository: it evaluates the flake on the target as root, and root there has no credential to fetch the private lens’s git+ssh://gitea.proton flake inputs. Use proton-deploy for those, which keeps evaluation on the workstation where the credentials live.

by machine type

Instructions across various kinds of machines are not consistent due to bootstrapping. See also All Machine Types.

BIOS systems

Not a lot must be done here to accommodate BIOS, since a bootable partition is simply bootable. There might have to be some adjustments made in the BIOS settings. Despite how simple this process is, it is essentially dead already. New systems will use UEFI and thus see UEFI systems.

UEFI systems

Unfortunately you cannot create a bootable image and simply slap it into a UEFI system, because that UEFI system has to be told where to boot. I’ve read that one can simply create a boot partition and put certain files in the right place. Nix does this, or can be taught to do this. In any case, I have verified the magic files went into the magic locations, but still no joy.

Instead, boot into the system using the detachable USB drive. The host is called nucleus.proton on boot. From there, you should be able to do a remote deployment. Unfortunately proton-deploy does not accommodate this activity yet. Until that feature is supported, you can use remote-deploy --enter-via nucleus.proton .... It would be desirable to use proton-deploy though, due to security concerns.

Once the machine state has been written, remove the USB drive and reboot the system.

The full UEFI/PC bootstrap runbook, from disk partitioning through the first agenix rekey, lives at bootstrap/bootstrap.org.

Raspberry Pi

The Raspberry Pi uses its own proprietary boot system. Sometimes it can use something called uboot, which may or may not be proprietary.

modules

rpi: initial deployment steps

For these, use the following invocation:

rpi-host-new $host

This generates the host’s real SSH keypair up front: the public half lands in secrets/$host-pub-key.pub, while the private half is kept encrypted to the master key as secrets/$host-host-key.age (requires the committed master public half - see secrets.org for the one-time setup). Commit the new and changed files under secrets/ before deploying.

Once that’s complete, use image-deploy thusly, with your SD card plugged in:

image-deploy --host $host --image tmp/images/host/sd-image/*.img.zst

Once it’s done, and the partition table looks fleshed out, remove the SD card (no eject/unmount needed). Plug it into the Pi and boot the Pi.

caffeinate proton-deploy switch $host

If secrets are missing on the freshly booted host (an empty /run/agenix), see the triage steps in bootstrap.org -> Bootstrap Pis.

Container Guests

host='my-host'; ssh-keygen -t ed25519 -N "" -f secrets/$host-pub-key

All Machine Types

You will need to deploy to nickel to have it pull in the new host for both its DNS settings, DHCP, and Prometheus exporting.

public key

You’ll need a public key to do rekeying of secrets. Unfortunately due to bootstrapping, you’ll need to generate a dummy key.

Raspberry Pi should see rpi: initial deployment steps instead.

host='host'; ssh-keygen -t ed25519 -N "" -f secrets/$host-pub-key
git add secrets/$host-pub-key*
host='host'; ssh-keyscan $host.proton | grep -o 'ssh-ed25519.*' > secrets/$host-pub-key.pub

devices

This covers miscellaneous devices on the network which need special instruction or understanding.

For IoT devices specifically – how they’re identified, provisioned onto the LAN, and controlled locally – see docs/iot.org.

Topology

Public/Private Service Split

All services, public and private, run on a single physical host (silicon) with a single network interface. Rather than exposing the host’s primary IP to the internet, a secondary IP address is bound to the same interface and the router is configured to port-forward external traffic only to that address.

The two addresses serve completely separate roles:

AddressRoleReachable fromCertificate source
192.168.254.9PrivateLAN onlyInternal CA
192.168.254.100PublicLAN + InternetACME (DNS-01)

nginx listens on each address independently. A virtual host bound to the private address is unreachable from the internet even if the router were misconfigured, because an nftables rule drops any packet arriving from a non-RFC-1918 source address destined for port 443 on .9. The rule runs at a higher priority than the default NixOS firewall chain, so it takes effect before any ACCEPT rule could apply.

The port-forward mapping on the router is maintained automatically via UPnP. A systemd timer fires shortly after boot and every twelve hours thereafter, re-registering the mapping so it survives router reboots.

The diagram below illustrates the traffic paths. The named source block is tangled to docs/topology.d2; the two shell blocks that follow it regenerate the light and dark SVGs in docs/ – execute those to refresh the diagram after any topology change.

Nix

Binary cache

Silicon serves two LAN caches: Attic at https://attic.proton/proton for locally built paths, and ncps at https://ncps.proton as a pull-through for cache.nixos.org. Every host substitutes from both. Layout, token minting, hit/miss checks, and signing-key rotation are in docs/nix-cache.org.

Rapid Package Updates

Some packages need to be updated independently of nixpkgs – for example, to pick up a bug fix that landed upstream before the pinned nixpkgs revision caught up. Bumping all of nixpkgs for a single package is risky, so instead we pin just the affected package’s version and hash in static.nix and override it in an overlay.

The three parts of this pattern are:

static.nix
Holds the version and hash for each pinned package. Both fields are updated together so the overlay always sees a consistent pair.
overlays/<pkg>.nix
Calls overrideAttrs on the nixpkgs derivation, replacing version and src with the values from static.nix. The overlay is wired into overlays/default.nix.
scripts/<pkg>-update
A shell script that fetches the latest upstream version, prefetches its hash with nix-prefetch-url, converts to SRI with nix hash to-sri, and writes both values back into static.nix via nix-editor.

To update a package, run its script and then redeploy:

scripts/firefox-bin-update
proton-deploy switch "$(hostname -s)"

Current packages using this pattern: claude-code, firefox-bin, makemkv, opencode, opencode-bun, stats, yt-dlp, zoom-us.

blackhole (the BlackHole 2ch macOS audio driver) pins version + hash in static.nix with a scripts/blackhole-update bump script in this same spirit, but deviates from the three-part shape above: it has no overlays/<pkg>.nix because the artifact is not a usable nixpkgs package but a notarized .pkg that Apple’s installer lays into /Library/Audio/Plug-Ins/HAL as root. It is therefore installed by a darwin-modules/blackhole.nix LaunchDaemon (the mechanism from the Evergreen Packages section) while keeping the pinned-version philosophy here – a hybrid, because BlackHole both publishes a versioned, hashable URL (so we pin) and ships no self-updater (so a bump must come from Nix, and the bootstrapper’s idempotency check is version-aware, not existence-only). The from-source default is waived for it under the binary-download exception below: a CoreAudio HAL driver must be Developer-ID signed and notarized to load under SIP, which we cannot reproduce – see the module header for the full justification.

Prefer from-source builds over direct binary downloads

This pattern comes in two shapes, and the choice between them is a policy, not a convenience call:

  • From-source (preferred, default). Pin version plus whatever fixed-output hashes the build needs (e.g. a srcHash and a vendored-deps hash such as nodeModulesHash or cargoHash), and build the package from its published source. When nixpkgs already builds the package from source, vendor its derivation under derivations/<pkg>/default.nix (an unaltered copy, save a header comment, is fine) and override the pinned fields from static.nix. opencode is the exemplar.
  • Direct binary download (exception, requires sign-off). Fetch a prebuilt artifact and install it without compiling. Only choose this when both of the following hold, and a human has explicitly approved it:
    1. We trust the source enough to run its bytes unbuilt (a first-party vendor artifact – npm package, signed release – not a random mirror), and
    2. Building from source is genuinely impractical – the source is closed (e.g. claude-code), or the from-source build is infeasible for reasons that must be spelled out in loads of comments in the overlay/derivation.

    The justification belongs in the file as comments, not just in a commit message. Absent an approved, documented reason, default to from-source even when a prebuilt binary would be less work.

The reason for the bias: a from-source build is auditable and reproducible from inputs we can read; a prebuilt binary asks us to trust opaque bytes and usually must be re-pinned per platform. “It was easier” is not a sufficient reason to take that trade.

Evergreen Packages

The Rapid Package Updates pattern above pins an exact version and hash and rebuilds when you bump them. That depends on upstream publishing a stable, addressable artifact: a URL that names a version, and bytes that do not change underneath it. Some vendors do not cooperate. They serve a single rolling, unversioned installer URL – names like latest, lkg (“last known good”), or a bare download endpoint – and overwrite those bytes in place whenever they ship. There is no version in the URL to record, and the hash changes silently and unpredictably.

Pinning such a package by hash is actively harmful: every silent upstream rotation makes the hash mismatch, which breaks the Nix build at the worst possible time (mid-deploy, unrelated to your change). Worse, some of these vendors force-update their app at runtime regardless of what you installed, so a pinned store copy would be stale the moment it launched.

For these we invert the model. Instead of pinning the artifact and owning the update cadence, we install imperatively, once, and delegate updates to the vendor’s own updater. The Nix configuration’s job shrinks to “make sure it is installed”; staying current is the vendor’s problem, exactly as it would be on a hand-managed machine.

The pattern has three parts, all on the macOS (nix-darwin) side:

darwin-modules/<pkg>.nix
A module exposing a programs.<pkg>.enable option (and usually appPath / installerUrl options with sensible defaults). Enabling it installs a launchd job; importing it without enabling does nothing. Wire the import into darwin.nix and set programs.<pkg>.enable = true on the host that wants it.
The bootstrapper
A writeShellScript the job runs. It must be idempotent and self-healing: first check whether the app already exists at appPath and exit 0 immediately if so (this is the common case at every boot/login – cheap, and it re-installs automatically if the app is ever deleted). Otherwise download the rolling URL to a scratch dir (use curl --fail so a 4xx/5xx becomes a non-zero exit and retries instead of “installing” an error page) and install it. Fetch tools from nixpkgs (pkgs.curl, pkgs.coreutils) rather than the BSD /usr/bin versions so the long-form flags exist and behave predictably. The install step depends on the artifact format:
  • A .app in a .dmg or archive – unpack and copy with ditto (it preserves the code signature and extended attributes a plain cp would strip). This needs no root, so the job can be a LaunchAgent.
  • A .pkg installer – run it with /usr/sbin/installer -package <pkg> -target /. This writes to root-owned locations and runs the vendor’s pre/post-install scripts, so it needs root and the job must be a LaunchDaemon.
The launchd job
Declared as launchd.agents.<name> (runs in the user’s GUI session, no root – pair with the ditto install) or launchd.daemons.<name> (runs as root at boot – pair with the installer install). Set RunAtLoad = true so it runs on each login/boot, and KeepAlive = false for a one-shot. If the install can fail transiently (e.g. a daemon firing at boot before the network is up), use KeepAlive = { SuccessfulExit = false; } instead – launchd then relaunches only while the last run failed and stops once it exits 0 – and add a ThrottleInterval (seconds) so a persistently dead URL does not loop tightly. Log to /tmp/<name>.log via StandardOutPath / StandardErrorPath for post-mortem.

To use an evergreen package, enable it on the host and deploy; the bootstrapper installs it on the next login (agent) or boot (daemon), and the vendor’s updater keeps it current thereafter. To force a clean reinstall, delete the app bundle and re-trigger the job (log out/in, reboot, or reload the job) – the idempotent check will see it missing and fetch the current build.

The trade-off versus Rapid Package Updates is deliberate: you give up reproducibility (two machines enabling the same package on different days can end up on different builds) in exchange for builds that never break on an upstream rotation and an app that actually stays current. Reach for this only when the vendor leaves no stable, hashable artifact to pin; prefer Rapid Package Updates whenever a versioned URL exists.

Current packages using this pattern: steam (a LaunchAgent that ditto~s Valve's rolling ~Steam.app launcher into /Applications and lets Steam self-update), and microsoft-teams (a LaunchDaemon that runs Microsoft’s rolling .pkg installer and lets Microsoft AutoUpdate keep Teams current).

Adding Services

Short of any community standing on what these should be called, we have two rough categories of Nix files we use for host configurations: A “module” and a “config”.

module
A module has options and config attributes, and declares its own options. The config section interprets those options into a configuration. Generally speaking, these modules will be ready to get contributed to nixpkgs. Importing such a module, sans any configuration for it, does nothing but makes the options available. So we gather up all modules and include them in the relevant host. nixos-modules contains all modules for NixOS, and should be imported in ./nixos-modules/linux-host.nix. There is also a darwin-modules which gets included in ./darwin.nix.
config
A config is relative inert, and it contains settings. It could dynamically determine settings and that’s fine, but it doesn’t declare options. Generally, a config is a place where we put the specifics for a service. Oftentimes a module will have an accompanying config. There is a series of config directories: darwin-configs, home-configs, and nixos-configs. They should go in there, and hosts or home profiles should pull them in as needed. Importing such a config applies its settings. You can find a sample nixos-config in ./nixos-configs/sample.nix. It contains patterns we typically find in our configs.

Exposing External Services Safely

Some services are meant to be reachable from the public internet, while others should stay private to the LAN. Both categories run on the same host, but the configuration described here keeps them completely separated at the network level. For the big picture of how the two IPs are structured, see Public/Private Service Split.

Configuring the Host

The split relies on two distinct IP addresses bound to the same network interface. The primary address carries private services; a secondary address is reserved for anything that should be externally reachable. The secondary address is declared directly on the host’s facts entry so that the DNS server and interface configuration both derive from a single source of truth:

silicon = {
  ipv4 = 9;                  # primary, private
  extraAddresses = {
    silicon-external = 100;  # secondary, public -- gets a DNS A record
  };
  # ...
};

With the secondary IP in place, declare a domain-suffix policy that maps a real TLD to the public address. Every FQDN registered under that suffix inherits the address and certificate source automatically:

services.https.domains = {
  # Private services -- all *.proton FQDNs listen on the LAN-only IP.
  "proton" = {
    addr       = "192.168.254.9";
    certSource = "internal-ca";
  };
  # Public services -- all *.example.com FQDNs listen on the public IP.
  "example.com" = {
    addr       = "192.168.254.100";
    certSource = "acme";
  };
};

Using certSource = "acme" requires the ACME module to be configured with a DNS provider capable of DNS-01 challenges (we use Porkbun). With DNS-01 the host does not need to be reachable on port 80 to obtain a certificate, which means certs can be provisioned before the domain is publicly live.

The router’s port-forward mapping needs to stay registered. We use the UPnP port-forward module to automate this; it registers the mapping on boot and refreshes it on a timer so it survives router reboots:

services.upnp-portforward = {
  enable       = true;
  addr         = "192.168.254.100";  # secondary IP to forward to
  externalPort = 443;
  localPort    = 443;
};

If the router does not support UPnP IGD, the port forward must be created manually in the router’s administration UI instead.

Configuring the Service

Once the domain policy is in place, adding a service for public access looks identical to adding a private one. Declare the FQDN under services.https.fqdns; the domain-suffix policy determines which IP and certificate source to use:

services.https.fqdns."myapp.example.com" = {
  internalPort = 8080;
};

Because the FQDN ends in .example.com, the system uses the policy declared above: nginx listens on 192.168.254.100:443, and an ACME certificate is requested automatically via DNS-01 the next time the ACME renewal runs.

Two external steps remain outside of NixOS configuration:

  1. Create a DNS A record at the registrar pointing myapp.example.com to the router’s WAN IP address.
  2. Ensure the router forwards TCP 443 to 192.168.254.100 – either through the UPnP module above or by a static rule in the router’s UI.

Secrets

Secrets are managed via agenix and agenix-rekey.

Generating Secrets

To generate secrets, and rekey them, use:

agenix rekey generate --rekey --add-to-git

This must be run by a human. If you’re not a human, ask the human to run it.

If you must regenerate secrets, remove the base secret file, and then ask your human to run the above invocation.

Issued Tokens

Tokens a third-party service issues (API keys, personal access tokens) can’t be generated, so they live under secrets/issued/ as <service>-<host-id>-<purpose>.age, one per consuming host so that revoking a token never reaches past that host. Services with several instances put the instance name after the service: grafana-<instance>-<host-id>-mcp-token.age. Store a freshly issued one with:

agenix edit secrets/issued/gitea-scandium-mcp-token.age
agenix rekey --add-to-git

The rekeyed copy is named <hash>-<secret-name>.age, where the hash covers the host key and the token bytes but not the file name. Renaming an issued file therefore needs no rekey, while renaming the age.secrets attribute does.

Wiring Secrets to Services

To wire up a secret to a service, you need to populate the service’s LoadCredentials list with your secrets. This creates a mapping of the secret from /run/agenix/<secret> to /run/credentials/%C/<secret>, where %C is a magic variable for systemd to expand into the unit name.

LoadCredentials secrets cannot be read, even by root.

If the service does not accept secret files, and filing a PR to change that seems implausible, look into adding an ExecStartPre which maps these over to environment variables.

If environment variables cannot be supported, then look to ./agenix/secret-template-file.nix which can generate arbitrary files which have secrets. You will also need to setup a restartTriggers to include the secret, so changes to the secret provoke a restart of the service.

There is also ./agenix/environment-file-secret.nix if the service accepts an environment-file.

Generations

List generations

# Alas, this isn't supported yet: https://github.com/NixOS/nix/pull/6911
export NO_COLOR=1
nix profile history --profile /nix/var/nix/profiles/system

Version �[1m114�[0m (2024-03-07) <- 113: No changes.

Version �[1m115�[0m (2024-03-07) <- 114: No changes.

Version �[1m116�[0m (2024-03-07) <- 115: No changes.

Version �[1m117�[0m (2024-03-07) <- 116: No changes.

Version �[1m118�[0m (2024-03-20) <- 117: No changes.

Version �[1m119�[0m (2024-03-31) <- 118: No changes.

Version �[1m120�[0m (2024-03-31) <- 119: No changes.

Version �[1m121�[0m (2024-03-31) <- 120: No changes.

Version �[1m122�[0m (2024-03-31) <- 121: No changes.

Version �[1m123�[0m (2024-03-31) <- 122: No changes.

Version �[1m124�[0m (2024-04-02) <- 123: No changes.

Version �[1m125�[0m (2024-04-02) <- 124: No changes.

Version �[1m126�[0m (2024-04-05) <- 125: No changes.

Version �[1m127�[0m (2024-05-14) <- 126: No changes.

Version �[1m128�[0m (2024-05-14) <- 127: No changes.

Version �[1m129�[0m (2024-05-14) <- 128: No changes.

Version �[1m130�[0m (2024-05-15) <- 129: No changes.

Version �[1m131�[0m (2024-05-15) <- 130: No changes.

Version �[1m132�[0m (2024-05-15) <- 131: No changes.

Version �[1m133�[0m (2024-05-15) <- 132: No changes.

Version �[1m134�[0m (2024-05-15) <- 133: No changes.

Version �[1m135�[0m (2024-05-15) <- 134: No changes.

Version �[1m136�[0m (2024-05-18) <- 135: No changes.

Version �[1m137�[0m (2024-05-21) <- 136: No changes.

Version �[1m138�[0m (2024-05-21) <- 137: No changes.

Version �[1m139�[0m (2024-05-21) <- 138: No changes.

Version �[1m140�[0m (2024-05-21) <- 139: No changes.

Version �[1m141�[0m (2024-05-21) <- 140: No changes.

Version �[1m142�[0m (2024-05-21) <- 141: No changes.

Version �[1m143�[0m (2024-05-21) <- 142: No changes.

Version �[1m144�[0m (2024-05-22) <- 143: No changes.

Version �[1m145�[0m (2024-05-22) <- 144: No changes.

Version �[1m146�[0m (2024-05-22) <- 145: No changes.

Version �[1m147�[0m (2024-05-22) <- 146: No changes.

Version �[1m148�[0m (2024-05-22) <- 147: No changes.

Version �[1m149�[0m (2024-05-23) <- 148: No changes.

Version �[1m150�[0m (2024-05-23) <- 149: No changes.

Version �[1m151�[0m (2024-05-23) <- 150: No changes.

Version �[1m152�[0m (2024-05-23) <- 151: No changes.

Version �[1m153�[0m (2024-05-23) <- 152: No changes.

Version �[1m154�[0m (2024-05-23) <- 153: No changes.

Version �[1m155�[0m (2024-05-23) <- 154: No changes.

Version �[1m156�[0m (2024-05-23) <- 155: No changes.

Version �[1m157�[0m (2024-05-23) <- 156: No changes.

Version �[1m158�[0m (2024-05-23) <- 157: No changes.

Version �[1m159�[0m (2024-05-23) <- 158: No changes.

Version �[1m160�[0m (2024-05-23) <- 159: No changes.

Version �[1m161�[0m (2024-05-23) <- 160: No changes.

Version �[1m162�[0m (2024-05-26) <- 161: No changes.

Version �[1m163�[0m (2024-05-26) <- 162: No changes.

Version �[1m164�[0m (2024-05-27) <- 163: No changes.

Version �[1m165�[0m (2024-05-27) <- 164: No changes.

Version �[1m166�[0m (2024-05-27) <- 165: No changes.

Version �[1m167�[0m (2024-05-27) <- 166: No changes.

Version �[1m168�[0m (2024-05-27) <- 167: No changes.

Version �[1m169�[0m (2024-05-27) <- 168: No changes.

Version �[1m170�[0m (2024-05-27) <- 169: No changes.

Version �[1m171�[0m (2024-05-27) <- 170: No changes.

Version �[1m172�[0m (2024-05-27) <- 171: No changes.

Version �[1m173�[0m (2024-05-27) <- 172: No changes.

Version �[1m174�[0m (2024-05-27) <- 173: No changes.

Version �[1m175�[0m (2024-05-27) <- 174: No changes.

Version �[1m176�[0m (2024-05-27) <- 175: No changes.

Version �[1m177�[0m (2024-05-27) <- 176: No changes.

Version �[1m178�[0m (2024-05-27) <- 177: No changes.

Version �[1m179�[0m (2024-05-27) <- 178: No changes.

Version �[1m180�[0m (2024-05-28) <- 179: No changes.

Version �[1m181�[0m (2024-05-28) <- 180: No changes.

Version �[1m182�[0m (2024-05-28) <- 181: No changes.

Version �[1m183�[0m (2024-05-28) <- 182: No changes.

Version �[1m184�[0m (2024-05-28) <- 183: No changes.

Version �[1m185�[0m (2024-05-28) <- 184: No changes.

Version �[1m186�[0m (2024-05-28) <- 185: No changes.

Version �[1m187�[0m (2024-05-31) <- 186: No changes.

Version �[1m188�[0m (2024-05-31) <- 187: No changes.

Version �[1m189�[0m (2024-05-31) <- 188: No changes.

Version �[1m190�[0m (2024-06-02) <- 189: No changes.

Version �[1m191�[0m (2024-06-02) <- 190: No changes.

Version �[1m192�[0m (2024-06-02) <- 191: No changes.

Version �[1m193�[0m (2024-06-03) <- 192: No changes.

Version �[1m194�[0m (2024-06-03) <- 193: No changes.

Version �[1m195�[0m (2024-06-03) <- 194: No changes.

Version �[1m196�[0m (2024-06-06) <- 195: No changes.

Version �[1m197�[0m (2024-06-06) <- 196: No changes.

Version �[1m198�[0m (2024-06-06) <- 197: No changes.

Version �[1m199�[0m (2024-06-06) <- 198: No changes.

Version �[1m200�[0m (2024-06-07) <- 199: No changes.

Version �[1m201�[0m (2024-06-07) <- 200: No changes.

Version �[1m202�[0m (2024-06-07) <- 201: No changes.

Version �[1m203�[0m (2024-06-07) <- 202: No changes.

Version �[1m204�[0m (2024-06-07) <- 203: No changes.

Version �[1m205�[0m (2024-06-07) <- 204: No changes.

Version �[1m206�[0m (2024-06-10) <- 205: No changes.

Version �[1m207�[0m (2024-06-10) <- 206: No changes.

Version �[1m208�[0m (2024-06-10) <- 207: No changes.

Version �[1m209�[0m (2024-06-10) <- 208: No changes.

Version �[1m210�[0m (2024-06-11) <- 209: No changes.

Version �[1m211�[0m (2024-06-14) <- 210: No changes.

Version �[1m212�[0m (2024-06-14) <- 211: No changes.

Version �[1m213�[0m (2024-06-14) <- 212: No changes.

Version �[1m214�[0m (2024-06-18) <- 213: No changes.

Version �[1m215�[0m (2024-06-22) <- 214: No changes.

Version �[1m216�[0m (2024-06-29) <- 215: No changes.

Version �[1m217�[0m (2024-06-29) <- 216: No changes.

Version �[1m218�[0m (2024-06-29) <- 217: No changes.

Version �[1m219�[0m (2024-06-29) <- 218: No changes.

Version �[1m220�[0m (2024-06-29) <- 219: No changes.

Version �[1m221�[0m (2024-06-29) <- 220: No changes.

Version �[1m222�[0m (2024-06-29) <- 221: No changes.

Version �[1m223�[0m (2024-06-29) <- 222: No changes.

Version �[1m224�[0m (2024-06-29) <- 223: No changes.

Version �[1m225�[0m (2024-06-29) <- 224: No changes.

Version �[1m226�[0m (2024-06-29) <- 225: No changes.

Version �[1m227�[0m (2024-06-30) <- 226: No changes.

Version �[1m228�[0m (2024-07-03) <- 227: No changes.

Version �[1m229�[0m (2024-07-03) <- 228: No changes.

Version �[1m230�[0m (2024-07-03) <- 229: No changes.

Version �[1m231�[0m (2024-07-03) <- 230: No changes.

Version �[1m232�[0m (2024-07-03) <- 231: No changes.

Version �[1m233�[0m (2024-07-03) <- 232: No changes.

Version �[1m234�[0m (2024-07-04) <- 233: No changes.

Version �[1m235�[0m (2024-07-04) <- 234: No changes.

Version �[1m236�[0m (2024-07-04) <- 235: No changes.

Version �[1m237�[0m (2024-07-04) <- 236: No changes.

Version �[1m238�[0m (2024-07-04) <- 237: No changes.

Version �[1m239�[0m (2024-07-06) <- 238: No changes.

Version �[1m240�[0m (2024-07-06) <- 239: No changes.

Version �[1m241�[0m (2024-07-09) <- 240: No changes.

Version �[1m242�[0m (2024-07-09) <- 241: No changes.

Version �[1m243�[0m (2024-07-10) <- 242: No changes.

Version �[1m244�[0m (2024-07-14) <- 243: No changes.

Version �[1m245�[0m (2024-07-24) <- 244: No changes.

Version �[1m246�[0m (2024-07-24) <- 245: No changes.

Version �[1m247�[0m (2024-07-24) <- 246: No changes.

Version �[1m248�[0m (2024-07-24) <- 247: No changes.

Version �[1m249�[0m (2024-07-24) <- 248: No changes.

Version �[1m250�[0m (2024-08-09) <- 249: No changes.

Version �[1m251�[0m (2024-08-10) <- 250: No changes.

Version �[1m252�[0m (2024-08-13) <- 251: No changes.

Version �[1m253�[0m (2024-08-13) <- 252: No changes.

Version �[1m254�[0m (2024-08-21) <- 253: No changes.

Version �[1m255�[0m (2024-08-24) <- 254: No changes.

Version �[1m256�[0m (2024-08-24) <- 255: No changes.

Version �[1m257�[0m (2024-08-24) <- 256: No changes.

Version �[1m258�[0m (2024-08-28) <- 257: No changes.

Version �[1m259�[0m (2024-08-29) <- 258: No changes.

Version �[1m260�[0m (2024-08-31) <- 259: No changes.

Version �[1m261�[0m (2024-09-03) <- 260: No changes.

Version �[1m262�[0m (2024-09-03) <- 261: No changes.

Version �[1m263�[0m (2024-09-03) <- 262: No changes.

Version �[1m264�[0m (2024-09-03) <- 263: No changes.

Version �[1m265�[0m (2024-09-03) <- 264: No changes.

Version �[1m266�[0m (2024-09-03) <- 265: No changes.

Version �[1m267�[0m (2024-09-03) <- 266: No changes.

Version �[1m268�[0m (2024-09-03) <- 267: No changes.

Version �[1m269�[0m (2024-09-03) <- 268: No changes.

Version �[1m270�[0m (2024-09-03) <- 269: No changes.

Version �[1m271�[0m (2024-09-03) <- 270: No changes.

Version �[1m272�[0m (2024-09-03) <- 271: No changes.

Version �[1m273�[0m (2024-09-04) <- 272: No changes.

Version �[1m274�[0m (2024-09-05) <- 273: No changes.

Version �[1m275�[0m (2024-09-05) <- 274: No changes.

Version �[1m276�[0m (2024-09-05) <- 275: No changes.

Version �[1m277�[0m (2024-09-05) <- 276: No changes.

Version �[1m278�[0m (2024-09-05) <- 277: No changes.

Version �[1m279�[0m (2024-09-05) <- 278: No changes.

Version �[1m280�[0m (2024-09-05) <- 279: No changes.

Version �[1m281�[0m (2024-09-05) <- 280: No changes.

Version �[1m282�[0m (2024-09-05) <- 281: No changes.

Version �[1m283�[0m (2024-09-05) <- 282: No changes.

Version �[1m284�[0m (2024-09-05) <- 283: No changes.

Version �[1m285�[0m (2024-09-05) <- 284: No changes.

Version �[1m286�[0m (2024-09-05) <- 285: No changes.

Version �[1m287�[0m (2024-09-05) <- 286: No changes.

Version �[1m288�[0m (2024-09-05) <- 287: No changes.

Version �[1m289�[0m (2024-09-05) <- 288: No changes.

Version �[1m290�[0m (2024-09-05) <- 289: No changes.

Version �[1m291�[0m (2024-09-05) <- 290: No changes.

Version �[1m292�[0m (2024-09-06) <- 291: No changes.

Version �[1m293�[0m (2024-09-06) <- 292: No changes.

Version �[1m294�[0m (2024-09-06) <- 293: No changes.

Version �[1m295�[0m (2024-09-06) <- 294: No changes.

Version �[1m296�[0m (2024-09-06) <- 295: No changes.

Version �[1m297�[0m (2024-09-06) <- 296: No changes.

Version �[1m298�[0m (2024-09-06) <- 297: No changes.

Version �[1m299�[0m (2024-09-06) <- 298: No changes.

Version �[1m300�[0m (2024-09-06) <- 299: No changes.

Version �[1m301�[0m (2024-09-06) <- 300: No changes.

Version �[1m302�[0m (2024-09-06) <- 301: No changes.

Version �[1m303�[0m (2024-09-06) <- 302: No changes.

Version �[1m304�[0m (2024-09-06) <- 303: No changes.

Version �[1m305�[0m (2024-09-06) <- 304: No changes.

Version �[1m306�[0m (2024-09-06) <- 305: No changes.

Version �[1m307�[0m (2024-09-06) <- 306: No changes.

Version �[1m308�[0m (2024-09-06) <- 307: No changes.

Version �[1m309�[0m (2024-09-06) <- 308: No changes.

Version �[1m310�[0m (2024-09-06) <- 309: No changes.

Version �[1m311�[0m (2024-09-06) <- 310: No changes.

Version �[1m312�[0m (2024-09-06) <- 311: No changes.

Version �[1m313�[0m (2024-09-06) <- 312: No changes.

Version �[1m314�[0m (2024-09-06) <- 313: No changes.

Version �[1m315�[0m (2024-09-06) <- 314: No changes.

Version �[1m316�[0m (2024-09-06) <- 315: No changes.

Version �[1m317�[0m (2024-09-06) <- 316: No changes.

Version �[1m318�[0m (2024-09-06) <- 317: No changes.

Version �[1m319�[0m (2024-09-06) <- 318: No changes.

Version �[1m320�[0m (2024-09-06) <- 319: No changes.

Version �[1m321�[0m (2024-09-06) <- 320: No changes.

Version �[1m322�[0m (2024-09-06) <- 321: No changes.

Version �[1m323�[0m (2024-09-06) <- 322: No changes.

Version �[1m324�[0m (2024-09-06) <- 323: No changes.

Version �[1m325�[0m (2024-09-06) <- 324: No changes.

Version �[1m326�[0m (2024-09-06) <- 325: No changes.

Version �[1m327�[0m (2024-09-06) <- 326: No changes.

Version �[1m328�[0m (2024-09-06) <- 327: No changes.

Version �[1m329�[0m (2024-09-06) <- 328: No changes.

Version �[1m330�[0m (2024-09-06) <- 329: No changes.

Version �[1m331�[0m (2024-09-06) <- 330: No changes.

Version �[1m332�[0m (2024-09-06) <- 331: No changes.

Version �[1m333�[0m (2024-09-06) <- 332: No changes.

Version �[1m334�[0m (2024-09-06) <- 333: No changes.

Version �[1m335�[0m (2024-09-06) <- 334: No changes.

Version �[1m336�[0m (2024-09-06) <- 335: No changes.

Version �[1m337�[0m (2024-09-06) <- 336: No changes.

Version �[1m338�[0m (2024-09-06) <- 337: No changes.

Version �[1m339�[0m (2024-09-06) <- 338: No changes.

Version �[1m340�[0m (2024-09-06) <- 339: No changes.

Version �[1m341�[0m (2024-09-06) <- 340: No changes.

Version �[1m342�[0m (2024-09-13) <- 341: No changes.

Version �[1m343�[0m (2024-09-14) <- 342: No changes.

Version �[1m344�[0m (2024-09-14) <- 343: No changes.

Version �[1m345�[0m (2024-09-16) <- 344: No changes.

Version �[1m346�[0m (2024-09-16) <- 345: No changes.

Version �[1m347�[0m (2024-09-19) <- 346: No changes.

Version �[1m348�[0m (2024-09-19) <- 347: No changes.

Version �[1m349�[0m (2024-09-20) <- 348: No changes.

Version �[1m350�[0m (2024-09-20) <- 349: No changes.

Version �[1m351�[0m (2024-09-20) <- 350: No changes.

Version �[1m352�[0m (2024-09-20) <- 351: No changes.

Version �[1m353�[0m (2024-09-20) <- 352: No changes.

Version �[1m354�[0m (2024-09-20) <- 353: No changes.

Version �[1m355�[0m (2024-09-20) <- 354: No changes.

Version �[1m356�[0m (2024-09-20) <- 355: No changes.

Version �[1m357�[0m (2024-09-20) <- 356: No changes.

Version �[1m358�[0m (2024-09-20) <- 357: No changes.

Version �[1m359�[0m (2024-09-20) <- 358: No changes.

Version �[1m360�[0m (2024-09-20) <- 359: No changes.

Version �[1m361�[0m (2024-09-20) <- 360: No changes.

Version �[1m362�[0m (2024-09-20) <- 361: No changes.

Version �[1m363�[0m (2024-09-20) <- 362: No changes.

Version �[1m364�[0m (2024-09-20) <- 363: No changes.

Version �[1m365�[0m (2024-09-20) <- 364: No changes.

Version �[1m366�[0m (2024-09-20) <- 365: No changes.

Version �[1m367�[0m (2024-09-20) <- 366: No changes.

Version �[1m368�[0m (2024-09-20) <- 367: No changes.

Version �[1m369�[0m (2024-09-20) <- 368: No changes.

Version �[1m370�[0m (2024-09-20) <- 369: No changes.

Version �[1m371�[0m (2024-09-20) <- 370: No changes.

Version �[1m372�[0m (2024-09-23) <- 371: No changes.

Version �[1m373�[0m (2024-09-23) <- 372: No changes.

Version �[1m374�[0m (2024-09-23) <- 373: No changes.

Version �[1m375�[0m (2024-09-25) <- 374: No changes.

Version �[1m376�[0m (2024-09-25) <- 375: No changes.

Version �[32;1m377�[0m (2024-09-26) <- 376: No changes.

conditional values

lib.mkIf

lib.mkIf either includes the value given or an empty attrset depending on the evaluation of the condition. In other words, this is for any attrset. For lists, see lib.optionals.

let
  a = lib.mkIf true { foo = "bar"; } # Returns { foo = "bar"; }.
  b = lib.mkIf false { foo = "bar"; } # Returns {}.
  # Returns { foo = "bar"; baz = "qux";  }
  c = { foo = "bar" } // (lib.mkIf true { baz = "qux"; })
  # Returns { foo = "bar"; }
  d = { foo = "bar" } // (lib.mkIf false { baz = "qux"; })
in {}

lib.optionals

lib.optionals includes the provided list if the condition is true. If false, an empty list is given. For an attrset, see lib.mkIf.

let
  a = lib.optionals true [ "foo" ] # Returns [ "foo" ].
  b = lib.optionals false [ "foo" ] # Returns [].
  # Returns [ "foo" "bar" ].
  c = [ "foo" ] // (lib.optionals true [ "bar" ])
  # Returns [ "foo" ].
  d = [ "foo" ] // (lib.optionals false [ "bar" ])
in {}

options that may not exist

While I was working on comfyui and started using different nixpkgs versions across hosts, I started running into problems with shared modules. Some would set services.comfyui and attributes under it, and this would cause Nix evaluation failures for hosts that didn’t know about it in their nixpkgs.

First, find or create your imports for the module in question. Next, use lib.mkIf and check for the existence of the option with builtins.hasAttr "comfyui" options.services). Then, in the key, allow the key name to evaluate to null based on a check.

{ lib, options, ... }: {
  imports = [
    (lib.mkIf (builtins.hasAttr "comfyui" options.services) {
      # This is kind of magical.  See
      # https://nix.dev/manual/nix/2.17/language/values.html?highlight=coerced#attribute-set
      # but basically if the attribute name evaluates to null then the attribute
      # won't exist.  Without this hack, we get `The option `services.comfyui'
      # does not exist.`.  This is a special case and one cannot use null as a
      # key name.
      services.${
        if (builtins.hasAttr "comfyui" options.services)
        then "comfyui"
        else null
      } = {
        package = pkgs ? comfyui-rocm;
        rocmSupport = true;
      };
    })
  ];
}

This is the best way to avoid config.modules and lib.mkMerge while also keeping things relatively simple. I should make a helper for this though.

To include a package conditionally, use this:

imports = [
  # cyme isn't available on all versions of nixpkgs I use.
  (lib.mkIf (builtins.hasAttr "cyme" pkgs) {
    environment.systemPackages =
      if (builtins.hasAttr "cyme" pkgs)
      then [
        # Allows us to query the status of USB devices.  This uses lsusb or
        # systemprofile -json under the hood in a cross-platform manner.
        # Unfortunately it does not work on non-USB devices (like SD cards)
        # like one might think.  This is _not_ for storage devices (many
        # things imply it will work, but it won't).
        pkgs.cyme
      ]
      else []
    ;
  })
];

Options and Config

Option sets this repository declares are documented in docs/nix-options.org, one heading per option set with a generated Options section, so each module stays the single source of truth for its descriptions.

Utilities

Looking up NixOS config

This is for showing the current configuration values tied to a host.

This is a pure Flake environment, so nixos-options will never work. Instead you can query the Flake itself thusly:

host='silicon'
nix eval \
  --impure \
  --json \
  .#nixosConfigurations.$host.config.services.openssh.enable
true

Looking up NixOS options

This is for showing the documentation of options available for a host.

Note, while this does print something, it won’t show you that you’re looking at nested options (I don’t think it even prints them). Right now ([2026-01-19 Mon]) there is no solid utility for doing this short of going to https://search.nixos.org.

host='silicon'
nix eval \
  --impure \
  --json \
  --apply '
    opts:
      builtins.listToAttrs (map
        (n: {
          name = n;
          value = {
            description = (opts.${n}.description or null);
            type = (opts.${n}.type.name or null);
          };
        })
        (builtins.attrNames opts))
  ' \
  .#nixosConfigurations.$host.options.services.nginx \
  | jq '
  to_entries
  | map({
      name: .key,
      description: (.value.description // "<no description>"),
      type: (.value.type // "<unknown>"),
      default:
        (if .value.hasDefault
         then (.value.default // "<unset>")
         else "<no default>"
         end),
    })
  | sort_by(.name)
'
[
  {
    "name": "additionalModules",
    "description": "Additional [third-party nginx modules](https://www.nginx.com/resources/wiki/modules/)\nto install. Packaged modules are available in `pkgs.nginxModules`.\n",
    "type": "listOf",
    "default": "<no default>"
  },
  {
    "name": "appendConfig",
    "description": "Configuration lines appended to the generated Nginx\nconfiguration file. Commonly used by different modules\nproviding http snippets. {option}`appendConfig`\ncan be specified more than once and its value will be\nconcatenated (contrary to {option}`config` which\ncan be set only once).\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "appendHttpConfig",
    "description": "Configuration lines to be appended to the generated http block.\nThis is mutually exclusive with using config and httpConfig for\nspecifying the whole http block verbatim.\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "clientMaxBodySize",
    "description": "Set nginx global client_max_body_size.",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "commonHttpConfig",
    "description": "With nginx you must provide common http context definitions before\nthey are used, e.g. log_format, resolver, etc. inside of server\nor location contexts. Use this attribute to set these definitions\nat the appropriate location.\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "config",
    "description": "Verbatim {file}`nginx.conf` configuration.\nThis is mutually exclusive to any other config option for\n{file}`nginx.conf` except for\n- [](#opt-services.nginx.appendConfig)\n- [](#opt-services.nginx.httpConfig)\n- [](#opt-services.nginx.logError)\n\nIf additional verbatim config in addition to other options is needed,\n[](#opt-services.nginx.appendConfig) should be used instead.\n",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "defaultHTTPListenPort",
    "description": "If vhosts do not specify listen.port, use these ports for HTTP by default.\n",
    "type": "unsignedInt16",
    "default": "<no default>"
  },
  {
    "name": "defaultListen",
    "description": "If vhosts do not specify listen, use these addresses by default.\nThis option takes precedence over {option}`defaultListenAddresses` and\nother listen-related defaults options.\n",
    "type": "listOf",
    "default": "<no default>"
  },
  {
    "name": "defaultListenAddresses",
    "description": "If vhosts do not specify listenAddresses, use these addresses by default.\nThis is akin to writing `defaultListen = [ { addr = \"0.0.0.0\" } ]`.\n",
    "type": "listOf",
    "default": "<no default>"
  },
  {
    "name": "defaultMimeTypes",
    "description": "Default MIME types for NGINX, as MIME types definitions from NGINX are very incomplete,\nwe use by default the ones bundled in the mailcap package, used by most of the other\nLinux distributions.\n",
    "type": "path",
    "default": "<no default>"
  },
  {
    "name": "defaultSSLListenPort",
    "description": "If vhosts do not specify listen.port, use these ports for SSL by default.\n",
    "type": "unsignedInt16",
    "default": "<no default>"
  },
  {
    "name": "enable",
    "description": "Whether to enable Nginx Web Server.",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "enableQuicBPF",
    "description": "Enables routing of QUIC packets using eBPF. When enabled, this allows\nto support QUIC connection migration. The directive is only supported\non Linux 5.7+.\nNote that enabling this option will make nginx run with extended\ncapabilities that are usually limited to processes running as root\nnamely `CAP_SYS_ADMIN` and `CAP_NET_ADMIN`.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "enableReload",
    "description": "Reload nginx when configuration file changes (instead of restart).\nThe configuration file is exposed at {file}`/etc/nginx/nginx.conf`.\nSee also `systemd.services.*.restartIfChanged`.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "eventsConfig",
    "description": "Configuration lines to be set inside the events block.\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "experimentalZstdSettings",
    "description": "Enable alpha quality zstd module with recommended settings.\nLearn more about compression in Zstd format [here](https://github.com/tokers/zstd-nginx-module).\n\nThis adds `pkgs.nginxModules.zstd` to `services.nginx.additionalModules`.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "gitweb",
    "description": "<no description>",
    "type": "<unknown>",
    "default": "<no default>"
  },
  {
    "name": "group",
    "description": "Group account under which nginx runs.",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "httpConfig",
    "description": "Configuration lines to be set inside the http block.\nThis is mutually exclusive with the structured configuration\nvia virtualHosts and the recommendedXyzSettings configuration\noptions. See appendHttpConfig for appending to the generated http block.\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "logError",
    "description": "Configures logging.\nThe first parameter defines a file that will store the log. The\nspecial value stderr selects the standard error file. Logging to\nsyslog can be configured by specifying the “syslog:” prefix.\nThe second parameter determines the level of logging, and can be\none of the following: debug, info, notice, warn, error, crit,\nalert, or emerg. Log levels above are listed in the order of\nincreasing severity. Setting a certain log level will cause all\nmessages of the specified and more severe log levels to be logged.\nIf this parameter is omitted then error is used.\n",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "mapHashBucketSize",
    "description": "Sets the bucket size for the map variables hash tables. Default\nvalue depends on the processor’s cache line size.\n\nRefer to [the nginx docs on hashes](https://nginx.org/en/docs/hash.html)\nfor more information.\n",
    "type": "nullOr",
    "default": "<no default>"
  },
  {
    "name": "mapHashMaxSize",
    "description": "Sets the maximum size of the map variables hash tables.\n",
    "type": "nullOr",
    "default": "<no default>"
  },
  {
    "name": "package",
    "description": "Nginx package to use. This defaults to the stable version. Note\nthat the nginx team recommends to use the mainline version which\navailable in nixpkgs as `nginxMainline`.\nSupported Nginx forks include `angie`, `openresty` and `tengine`.\n",
    "type": "package",
    "default": "<no default>"
  },
  {
    "name": "preStart",
    "description": "Shell commands executed before the service's nginx is started.\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "prependConfig",
    "description": "Configuration lines prepended to the generated Nginx\nconfiguration file. Can for example be used to load modules.\n{option}`prependConfig` can be specified more than once\nand its value will be concatenated (contrary to {option}`config`\nwhich can be set only once).\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "proxyCache",
    "description": "<no description>",
    "type": "<unknown>",
    "default": "<no default>"
  },
  {
    "name": "proxyCachePath",
    "description": "Configure a proxy cache path entry.\nSee <https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_path> for documentation.\n",
    "type": "attrsOf",
    "default": "<no default>"
  },
  {
    "name": "proxyResolveWhileRunning",
    "description": "Resolves domains of proxyPass targets at runtime and not only at startup.\nThis can be used as a workaround if nginx fails to start because of not-yet-working DNS.\n\n:::{.warn}\n`services.nginx.resolver` must be set for this option to work.\n:::\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "proxyTimeout",
    "description": "Change the proxy related timeouts in recommendedProxySettings.\n",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "recommendedBrotliSettings",
    "description": "Enable recommended brotli settings.\nLearn more about compression in Brotli format [here](https://github.com/google/ngx_brotli/).\n\nThis adds `pkgs.nginxModules.brotli` to `services.nginx.additionalModules`.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "recommendedGzipSettings",
    "description": "Enable recommended gzip settings.\nLearn more about compression in Gzip format [here](https://docs.nginx.com/nginx/admin-guide/web-server/compression/).\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "recommendedOptimisation",
    "description": "Enable recommended optimisation settings.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "recommendedProxySettings",
    "description": "Whether to enable recommended proxy settings if a vhost does not specify the option manually.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "recommendedTlsSettings",
    "description": "Enable recommended TLS settings.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "recommendedUwsgiSettings",
    "description": "Whether to enable recommended uwsgi settings if a vhost does not specify the option manually.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "recommendedZstdSettings",
    "description": "<no description>",
    "type": "unspecified",
    "default": "<no default>"
  },
  {
    "name": "resolver",
    "description": "Configures name servers used to resolve names of upstream servers into addresses\n",
    "type": "submodule",
    "default": "<no default>"
  },
  {
    "name": "serverNamesHashBucketSize",
    "description": "Sets the bucket size for the server names hash tables. Default\nvalue depends on the processor’s cache line size.\n",
    "type": "nullOr",
    "default": "<no default>"
  },
  {
    "name": "serverNamesHashMaxSize",
    "description": "Sets the maximum size of the server names hash tables.\n",
    "type": "nullOr",
    "default": "<no default>"
  },
  {
    "name": "serverTokens",
    "description": "Show nginx version in headers and error pages.",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "sslCiphers",
    "description": "Ciphers to choose from when negotiating TLS handshakes.",
    "type": "nullOr",
    "default": "<no default>"
  },
  {
    "name": "sslDhparam",
    "description": "Path to DH parameters file.",
    "type": "nullOr",
    "default": "<no default>"
  },
  {
    "name": "sslProtocols",
    "description": "Allowed TLS protocol versions.",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "sso",
    "description": "<no description>",
    "type": "<unknown>",
    "default": "<no default>"
  },
  {
    "name": "stateDir",
    "description": "<no description>",
    "type": "unspecified",
    "default": "<no default>"
  },
  {
    "name": "statusPage",
    "description": "Enable status page reachable from localhost on http://127.0.0.1/nginx_status.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "streamConfig",
    "description": "Configuration lines to be set inside the stream block.\n",
    "type": "separatedString",
    "default": "<no default>"
  },
  {
    "name": "tailscaleAuth",
    "description": "<no description>",
    "type": "<unknown>",
    "default": "<no default>"
  },
  {
    "name": "typesHashMaxSize",
    "description": "Sets the maximum size of the types hash tables (`types_hash_max_size`).\nIt is recommended that the minimum size possible size is used.\nIf {option}`recommendedOptimisation` is disabled, nginx would otherwise\nfail to start since the mailmap `mime.types` database has more entries\nthan the nginx default value 1024.\n",
    "type": "positiveInt",
    "default": "<no default>"
  },
  {
    "name": "upstreams",
    "description": "Defines a group of servers to use as proxy target.\n",
    "type": "attrsOf",
    "default": "<no default>"
  },
  {
    "name": "user",
    "description": "User account under which nginx runs.",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "uwsgiResolveWhileRunning",
    "description": "Resolves domains of uwsgi targets at runtime\nand not only at start, you have to set\nservices.nginx.resolver, too.\n",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "uwsgiTimeout",
    "description": "Change the uwsgi related timeouts in recommendedUwsgiSettings.\n",
    "type": "str",
    "default": "<no default>"
  },
  {
    "name": "validateConfigFile",
    "description": "Whether to enable validating configuration with pkgs.writeNginxConfig.",
    "type": "bool",
    "default": "<no default>"
  },
  {
    "name": "virtualHosts",
    "description": "Declarative vhost config",
    "type": "attrsOf",
    "default": "<no default>"
  }
]

Developing on Third-Party Repositories

Working in a cloned repo that has no flake.nix without polluting its git history uses two complementary mechanisms:

  • .git/info/exclude is a per-clone gitignore that is never committed. Add local-only files here so they cannot accidentally be staged or committed upstream.
  • Nix’s flake evaluator only requires files to be staged (not committed), so git add flake.nix flake.lock makes them visible to nix develop without creating a commit.

Full per-project devShell

Use this when the project warrants its own pinned nixpkgs revision or project-specific tools.

echo "flake.nix" >> .git/info/exclude
echo "flake.lock" >> .git/info/exclude
echo ".envrc"    >> .git/info/exclude
# Write flake.nix, then stage it so Nix can evaluate it.
git add flake.nix flake.lock
echo "use flake ." > .envrc
direnv allow

Reusable devShell from dotfiles

Use this for a quick environment (e.g. “I just need Rust tooling”) without authoring a per-repo flake at all. Define named devShells in this flake and reference them by name.

echo ".envrc" >> .git/info/exclude
echo "use flake path:/Users/logan/dev/dotfiles#rust" > .envrc
direnv allow

TLS Certificate Trusting

iOS

  1. Email ./secrets/proton-ca.crt to the recipient.
  2. Open the email in iOS.
  3. Tap the name of the file (not the download button).
  4. Choose the current device to install the profile.
  5. Open the Settings app.
  6. Profile Download should appear just below the user account (should be the second entry). Go there.
  7. “Not verified” is normal/expected to see. Tap Install.
  8. Tap install again.
  9. Tap install yet again.
  10. Go to the root of the Settings app.
  11. Navigate to General -> About -> Certificate Trust Settings. The last entry will be at the bottom.
  12. Turn on the domain for the network (proton).
  13. Press Continue.
  14. Verify by navigating to a resource such as https://home-assistant.proton - it doesn’t matter if you can’t log in. You just need the page to load without a security warning.

Authentication

Architecture

Authentication is centralized: OpenLDAP holds the identity store, and Authelia is the OIDC/OAuth2 provider for browser-based SSO. Services either authenticate directly against LDAP (older pattern) or use Authelia as an OIDC identity provider (preferred going forward).

Browser / Client
     │
     ▼
Authelia (silicon, authelia.proton)
├─ OIDC authorization server
├─ Session management & access-control policy
└─ Delegates identity to OpenLDAP
     │
     ▼
OpenLDAP (silicon, ldap.proton:636, LDAPS only)
├─ dc=proton,dc=org
├─ ou=users  (person accounts + service accounts)
└─ ou=groups (groupOfNames; memberOf overlay)

Identity Store: OpenLDAP

The LDAP tree is facts-driven. nixos-modules/facts.nix is the single source of truth for human accounts, group memberships, and service endpoints. A Rust tool (ldap-reconciler) runs hourly on silicon, reading a JSON projection of facts.nix and reconciling the live directory against it.

  • Service accounts are declared inline in each service’s NixOS config via auth.ldap.users."<host>-<service>-service". agenix-rekey auto-generates plaintext and hashed password secrets.
  • Person account passwords are set once on creation and left alone by the reconciler (managed = false).
  • Secrets reach services via systemd LoadCredential, never via filesystem ownership.

OIDC Provider: Authelia

Authelia is configured at nixos-configs/authelia.nix. It reads users and groups from LDAP and issues OIDC tokens.

OIDC clients are generated dynamically from facts.network.services: any service with authentication = "oidc" gets a client entry. Confidential clients receive a client_secret_post credential; public clients (e.g. OpenHAB) use PKCE.

Access-control policy is also facts-driven: each service’s fqdn and groups list produce an Authelia rule granting one_factor to members of those groups.

Key limitation: Authelia can copy static LDAP attribute values into claims but has no expression engine to compute derived values (e.g. “is user in group X? emit role=admin”). Services that need role-in-claim must either accept manual post-login promotion or add a custom attribute to the LDAP schema.

Service Authentication Inventory

ServiceHostAuth todayOIDC-capableMigrate?Notes
ImmichsiliconOIDC✓DoneAdmin promoted manually in UI
GiteasiliconLDAP✓YesNeeds per-user DB update [fn:gitea]
MatrixsiliconLDAP✓Yesldap3 incompatible with MAS [fn:matrix]
GrafananickelLDAP✓Yesgroups must be in ID token [fn:grafana]
NextcloudcopperLDAPPartialNeutralAdmin group sync bug [fn:nextcloud]
OpenHABbromine-via proxyPlannedForward-auth; currently disabled
MeTubesiliconNonevia proxyN/AForward-auth; mind socket.io

Gitea migration notes

[fn:gitea] Existing accounts must be linked manually: set login_name to the OIDC sub value, login_type to 6 (OAuth), and login_source to the new authentication source ID in the database. Without this, OIDC logins create duplicate accounts. Upstream bug: admin/restricted status from the groups claim is only applied on the second login (gitea#32566).

Matrix/Synapse migration notes

[fn:matrix] The matrix-synapse-ldap3 plugin is incompatible with Matrix Authentication Service (MAS / MSC3861), the direction Synapse is heading. Migrate to the built-in oidc_providers with allow_existing_users: true and localpart_template: "{{ user.preferred_username }}" (Authelia exposes LDAP uid as preferred_username). Both LDAP and OIDC auth can coexist during the transition; remove ldap3 only after all users have logged in via OIDC at least once.

Grafana migration notes

[fn:grafana] Grafana evaluates role_attribute_path (JMESPath) against the ID token. Authelia 4.39+ puts groups in the UserInfo endpoint by default, not the ID token. If groups is absent and the JMESPath expression has a || 'Viewer' fallback, Grafana resolves it as Viewer without ever calling UserInfo. Fix: add a claims_policies entry for the Grafana client in authelia.nix that explicitly includes groups in the ID token.

Nextcloud migration notes

[fn:nextcloud] The user_oidc app can provision groups from the groups claim, but the admin group cannot be managed this way – members get removed on sync (open upstream issue as of 2025). A hybrid approach (LDAP for identity, OIDC for login) is the most practical path. Full cut-over requires accepting manual admin management. If Server-Side Encryption is ever enabled, OIDC is completely blocked (SSE requires cleartext passwords at login time).

Ollama

LLM inference is available at ollama.proton but the endpoint is not a direct Ollama server – it’s garage-queue, a capability-aware work queue that distributes inference requests across all hosts running Ollama.

Architecture

client (curl, gold-dig, etc.)
  │
  │  HTTPS
  ▼
ollama.proton (DNS → silicon)
  │
  │  nginx → Unix socket
  ▼
garage-queue-server (silicon)
  │
  │  NATS JetStream
  ▼
garage-queue-worker (titanium, arsenic, mac, …)
  │
  │  HTTP localhost:11434
  ▼
Ollama (local to worker)

What gets queued

Only POST /api/generate is routed through the queue. The server matches the incoming path against configured queue routes, extracts capability requirements from the JSON body (model tag + estimated VRAM), and enqueues the item. The connection is held open until a worker returns a result.

What does not work

Every other Ollama endpoint (GET /api/tags, POST /api/chat, etc.) returns a 405 (Method Not Allowed for GET) or 404 (no queue mapped for POST to unmapped paths) from garage-queue. There is currently no proxy pass-through for non-queued routes.

This means:

  • Clients cannot discover available models via ollama.proton/api/tags.
  • POST /api/chat does not work through the queue endpoint.

Workers and capabilities

Each host that runs Ollama also runs a garage-queue-worker. The worker advertises:

  • Tags – the model names from services.ollama.loadModels (e.g. llama3.1:8b, qwen2.5:14b).
  • Scalars – numeric capacities like vram_mb declared per-host.

When an inference request arrives, the server finds a worker whose advertised tags include the requested model and whose VRAM scalar meets the estimated requirement.

Model tiers

Models are configured in Nix by VRAM tier. Each tier imports the previous one:

TierFileAdds
8 GBollama-models-8gb-vram.nixdolphin3:8b, gemma3:4b, llama3.1:8b,
llama3.2:3b, mistral:7b, phi4-mini
12 GBollama-models-12gb-vram.nixgemma3:12b, qwen2.5:14b

A host imports the tier matching its GPU. The ollama-model-loader systemd service pulls models on boot.

Relevant files

FileRole
nixos-configs/garage-queue-server.nixServer + nginx on silicon
nixos-configs/garage-queue-worker.nixWorker config (all Ollama hosts)
nixos-configs/ollama.nixOllama service + model loader
nixos-configs/ollama-models-*-vram.nixPer-tier model lists
hosts/titanium.nix12 GB VRAM worker example

Direct access

To bypass the queue and hit Ollama directly (e.g. for /api/tags or debugging), SSH to the host and use localhost:11434:

ssh titanium.proton curl -s http://localhost:11434/api/tags | jq '.models[].name'

Known limitations

  • GET /api/tags is not supported through ollama.proton. garage-queue would need to synthesize this response by aggregating the model tags that workers have advertised, which is not yet implemented.
  • POST /api/chat is not queued. Adding it would require a second queue definition mirroring the /api/generate setup.
  • There is no transparent proxy fallback for non-queued routes.

Operations

Runbooks for recurring failures on this network, each with the symptom, the cause, and a link to the full write-up under docs/operations/, are indexed in docs/operations.org.

Graveyard

The graveyard/ directory preserves configurations for services that are no longer active. Files land here when the configuration could be useful as a future reference, when the service might be revisited, or when the history of how something was set up is worth retaining.

The directory structure mirrors the root of the repository. For example, a NixOS config that lived at nixos-configs/foo.nix moves to graveyard/nixos-configs/foo.nix.

Every file in the graveyard must have a TOMBSTONE comment block at the top of the file explaining why it was retired. The format is:

################################################################################
# TOMBSTONE -- Short description of why this is no longer active.
#
# What the service was and what it did.
#
# Reason for decommission.  Where its import was removed from, if applicable.
# This file is kept for historical reference.
################################################################################

About

Nix configuration for an entire fleet of macOS and NixOS hosts

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages