Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

dotfiles.nix wiki

Welcome to the dotfiles.nix documentation.

Nix Knowledge

Guides

Package broken, PR merged but no update yet?

Track PR with: https://nixpk.gs/pr-tracker.html

Repository Structure

This flake follows the dendritic pattern. flake.nix declares inputs and nothing else; every file under modules/ is a flake-parts module discovered automatically by import-tree.

.
├── flake.nix               # Inputs, then: mkFlake (import-tree ./modules)
├── vars/                   # Per-configuration identity (see modules/core/vars.nix)
├── packages/               # Own package definitions
└── modules/                # Every file here is a flake-parts module
    ├── flake/              # Flake-level plumbing: systems, lib, packages, devshell
    ├── core/               # Cross-cutting glue
    │   ├── aggregates.nix    # Placeholders for aggregates that may be empty
    │   ├── args.nix          # Supplies `inputs` / `baseLib` to exported modules
    │   ├── vars.nix          # Declares the `vars` option and its defaults
    │   ├── nixpkgs.nix       # All overlays, in one place so order is stable
    │   ├── home.nix          # home-manager basics for every user
    │   ├── darwin.nix        # darwin.base: nix-darwin + home-manager setup
    │   └── nixos.nix         # nixos.base: NixOS + home-manager setup
    ├── hosts/              # One directory per machine
    │   ├── JosefsMacBookPro/
    │   │   ├── default.nix   # The darwinSystem call
    │   │   ├── dock.nix      # This machine's dock layout
    │   │   ├── homebrew.nix
    │   │   └── packages.nix
    │   └── josef-nd1-gpu0/
    │       ├── default.nix   # The nixosSystem call
    │       ├── host.nix      # Bootloader, filesystems
    │       ├── hardware.nix  # hardware-configuration
    │       └── packages.nix
    ├── hardware/           # Hardware features, e.g. nvidia.nix
    ├── k9s.nix             # A feature small enough to be one file
    └── git/                # A feature with several files or assets
        ├── default.nix
        ├── ghq.nix
        └── personal.nix

Features are grouped by feature, not by platform or audience. A feature is a single <name>.nix file, or a <name>/ directory once it needs more than one file (extra modules, or assets such as kitty/scripts/).

A file says which configurations it applies to by writing into a named aggregate, and one file can write to several. Audiences do not need a file each:

# modules/git/default.nix
{
  flake.modules.homeManager.base = {vars, ...}: { programs.git = { /* ... */ }; };

  # mixing audiences in one file is fine, and preferred while it stays small
  flake.modules.homeManager.personal = {vars, ...}: { /* never exported */ };
}

Splitting a section into its own file is purely a size decision. It changes nothing about how the modules merge:

# modules/git/personal.nix
{
  flake.modules.homeManager.personal = {vars, ...}: { /* never exported */ };
}

Paths containing /_ are skipped by import-tree. That is how modules/editor/nvf/_parts/ stays out, since those files are imported as plain functions rather than being modules.

Finding things

Because audience is declared in the file rather than encoded in the path, the files belonging to one machine are spread across feature directories. To locate them:

$ just where josef-nd1-gpu0     # every file contributing to that aggregate
$ just aggregates               # every published aggregate name

See the README for the full aggregate table and how to consume this flake as a base for another configuration.

External Usage

This flake exposes its modules for consumption by other configurations, allowing a layered approach where dotfiles.nix provides the base.

Exported modules

Modules are published as flake.modules.<class>.<name>. Only the base aggregates are part of the public surface:

AggregateScope
generic.basecross-platform system config
darwin.basenix-darwin system + home-manager wiring
nixos.baseNixOS system + home-manager wiring
homeManager.basecross-platform home config
homeManager.darwin / homeManager.nixosplatform-specific home config

darwin.base and nixos.base each import generic.base and the matching homeManager.* aggregates, so a consumer imports exactly one module.

Everything else (generic.personal, homeManager.personal, the per-host aggregates, and opt-in capabilities such as nixos.nvidia) is deliberately unreachable from *.base and never reaches a downstream configuration.

lib additionally exposes mkHome, mkDotPath and mkOpAgentSock.

Example consumption

Point nixpkgs at this flake so you don’t end up with two nixpkgs in one closure, then import the aggregate and supply vars:

{
  inputs = {
    base.url = "github:ojsef39/dotfiles.nix";
    nixpkgs.follows = "base/nixpkgs";
    darwin.follows = "base/darwin";
  };

  outputs = {base, darwin, ...}: {
    darwinConfigurations.workMac = darwin.lib.darwinSystem {
      modules = [
        base.modules.darwin.base
        {nixpkgs.hostPlatform = "aarch64-darwin";}
        {
          vars = {
            user = {
              name = "jhofer";
              full_name = "Josef Hofer";
              email = "josef.hofer@example.com";
            };
            git = {
              ghq = "workspace";
              dotfiles = "git.example.com/jhofer/nix-work";
              url = "git.example.com";
            };
          };
        }
        ./work-specific-config.nix
      ];
    };
  };
}

There is no specialArgs: vars, inputs and baseLib are supplied by the imported modules themselves.

vars is a typed option (see modules/core/vars.nix). Only user.name, user.full_name, user.email and git.dotfiles are required; everything else has a default, and a missing key produces a named option error rather than a stray attribute ... missing. The type is freeform, so a consumer can keep its own private keys in the same attrset.

To drop something from the base, use the module system rather than forking. Base modules are written enable-style:

{lib, ...}: {
  home-manager.users.jhofer.programs.k9s.enable = lib.mkForce false;
}

Remote building

(with 1Password as SSH Agent)

nix build .#darwinConfigurations.JosefsMacBookPro.system --builders 'ssh://<user>@<ip> x86_64-linux,aarch64-darwin'

Caution

Make sure you ran sudo ssh <user>@<ip> first and accept the host key dialog, otherwise remote build will fail as that runs as root (nix daemon).

This only works because of modules/macos/system.nix:

      # Configure nix-daemon to use 1Password SSH agent for remote builders
      # Uses PlistBuddy to modify the Determinate-managed plist directly
      # DOCS: included in wiki/src/external-usage.md via the anchor above
      activationScripts = {
        preActivation.text = ''
          plist="/Library/LaunchDaemons/systems.determinate.nix-daemon.plist"
          desired="/Users/${vars.user.name}/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"

          current=$(/usr/libexec/PlistBuddy -c "Print :EnvironmentVariables:SSH_AUTH_SOCK" "$plist" 2>/dev/null || echo "")

          if [ "$current" != "$desired" ]; then
            /usr/libexec/PlistBuddy -c "Add :EnvironmentVariables dict" "$plist" 2>/dev/null || true
            /usr/libexec/PlistBuddy -c "Set :EnvironmentVariables:SSH_AUTH_SOCK '$desired'" "$plist" 2>/dev/null || \
            /usr/libexec/PlistBuddy -c "Add :EnvironmentVariables:SSH_AUTH_SOCK string '$desired'" "$plist"

            if launchctl print system/systems.determinate.nix-daemon &>/dev/null; then
              launchctl unload /Library/LaunchDaemons/systems.determinate.nix-daemon.plist
              launchctl load /Library/LaunchDaemons/systems.determinate.nix-daemon.plist
            fi
          fi
        '';

        # example for linux
        # systemd.services.nix-daemon = {
        #   environment = {
        #     SSH_AUTH_SOCK = "/run/user/${builtins.toString config.users.users.${username}.uid}/${
        #       config.home-manager.users.${username}.services.ssh-agent.socket
        #     }";
        #   };
        # };
      };

Setup & Deployment

This configuration is managed using nh (Nix Helper) and just.

#!/usr/bin/env just --justfile

alias d := deploy
alias u := upgrade

# macOS need nh darwin switch and NixOS needs nh os switch
nix_cmd := `if [ "$(uname)" = "Darwin" ]; then echo "darwin"; else echo "os"; fi`
# Configurations are named after the machine, so this needs no per-OS special
# case. -s because macOS `hostname` returns the FQDN (JosefsMacBookPro.local).
nix_host := `hostname -s`
# Use GITHUB_TOKEN from 1Password to prevent rate limiting (only if op is available and connected)
nix_flags := ```
  if [ "${GITHUB_ACTIONS:-}" != "true" ] && command -v op > /dev/null 2>&1; then
    token=$(op read op://Personal/GITHUB_TOKEN/no_access 2>/dev/null)
    if [ -n "$token" ]; then
      echo "--option access-tokens github.com=$token"
    fi
  fi
  ```

[doc('HELP')]
default:
    @just --list --list-prefix "    " --list-heading $'🔧 Available Commands:\n'

[group('nix')]
[doc('Deploy system configuration')]
deploy: lint
    # Deploying system configuration without update...
    @git pull --rebase --autostash || true
    @git add .
    @nh {{nix_cmd}} switch -a -H {{nix_host}} $NIX_GIT_PATH -- {{nix_flags}}

[group('nix')]
[doc('Deploy system configuration')]
deploy-update: lint
    # Deploying system configuration with update...
    @git pull --rebase --autostash || true
    @git add .
    @nh {{nix_cmd}} switch -u -a -H {{nix_host}} $NIX_GIT_PATH -- {{nix_flags}}

[group('nix')]
[doc('Upgrade refs and deploy')]
upgrade: lint
    @git pull --rebase --autostash || true
    @git add .
    @nh {{nix_cmd}} switch -a -H {{nix_host}} $NIX_GIT_PATH -- {{nix_flags}}
    @git add .
    @if git log -1 --pretty=%B | grep -q "chore(deps): updated inputs and refs"; then \
        echo "Amending previous dependency update commit..."; \
        git commit --amend --no-edit || true; \
    else \
        git commit -m "chore(deps): updated inputs and/or refs" || true; \
    fi


[group('nix')]
[doc('List module files that define or import an aggregate, e.g. `just where nvidia`')]
where aggregate:
    #!/usr/bin/env bash
    # Matches `flake.modules.<class>.<name>` (a definition) and `m.<class>.<name>`
    # (a host importing it), not bare occurrences of the word.
    def=$(grep -rlE "flake\.modules\.[a-zA-Z]+\.{{aggregate}}\b" --include='*.nix' modules || true)
    use=$(grep -rlE "\bm\.[a-zA-Z]+\.{{aggregate}}\b"            --include='*.nix' modules || true)
    if [ -z "$def$use" ]; then echo "nothing defines or imports '{{aggregate}}'"; exit 0; fi
    if [ -n "$def" ]; then echo "defined by:";  echo "$def" | sed 's/^/  /'; fi
    if [ -n "$use" ]; then echo "imported by:"; echo "$use" | sed 's/^/  /'; fi

[group('nix')]
[doc('List all published module aggregates')]
aggregates:
    @nix eval --json .#modules --apply 'm: builtins.mapAttrs (_: builtins.attrNames) m' | nix run nixpkgs#jq -- .

[group('maintain')]
[doc('Clean and optimise the nix store with nh')]
clean:
    @nh clean all -a -k 2 -K 7d

[group('maintain')]
[doc('Optimise the nix store')]
optimise:
    @nix store optimise -v

[group('maintain')]
[doc('Verify and repair the nix-store')]
repair:
    @sudo nix-store --verify --check-contents --repair || true

[group('maintain')]
[doc('Selectively rollback flake inputs')]
rollback:
    @./scripts/flake-rollback.fish

[group('lint')]
[doc('Lint all nix files using statix and deadnix')]
lint: format
    @nix run {{nix_flags}} nixpkgs#statix -- check .
    @nix run {{nix_flags}} nixpkgs#deadnix -- -eq .

[group('lint')]
[doc('Format files using alejandra')]
format:
    @nix run {{nix_flags}} nixpkgs#alejandra -- .

[group('lint')]
[doc('Show diff between current and commited changes')]
diff:
    git diff ':!flake.lock'

Docker - Building container images with Nix

Example flake showing buildLayeredImage and buildImage with Ubuntu base.

Try it out

cd wiki/docker && nix develop -c $SHELL

or with nix-output-monitor (nom develop -c $SHELL) for a nice overview of the build progress

This will build and load two Docker images:

  • nix-shell:latest - Pure Nix image using buildLayeredImage
  • ubuntu-nix:latest - Ubuntu base with Nix tools using buildImage

flake.nix

{
  description = "Nix Docker image examples";

  inputs = {
    nixpkgs.url = "https://flakehub.com/f/JHOFER-Cloud/NixOS-nixpkgs/0.1.tar.gz"; # usually github:NixOS/nixpkgs/nixpkgs-unstable
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = {
    nixpkgs,
    flake-utils,
    ...
  }:
    flake-utils.lib.eachDefaultSystem (system: let
      pkgs = nixpkgs.legacyPackages.${system};

      # Get Linux packages - works on macOS with Determinate's built-in linux-builder
      # This uses the linux-builder to build natively for Linux (fast, fully cached)
      linuxPkgs = import pkgs.path {system = "x86_64-linux";};

      # Alternative: Cross-compile to Linux (no cache, slower first build)
      # linuxPkgs = pkgs.pkgsCross.musl64;

      # Example 1: buildLayeredImage - pure Nix, from scratch
      layeredImage = pkgs.dockerTools.buildLayeredImage {
        name = "nix-shell";
        tag = "latest";
        contents = [linuxPkgs.busybox];
        config.Cmd = ["/bin/sh"];
      };

      # Example 2: buildImage with Ubuntu base
      ubuntuBase = pkgs.dockerTools.pullImage {
        imageName = "ubuntu";
        imageDigest = "sha256:cd1dba651b3080c3686ecf4e3c4220f026b521fb76978881737d24f200828b2b";
        sha256 = "sha256-9vmJV6M21tZPk39lCCdCrwHR55gNzO1ka2De51IR9qs=";
        finalImageTag = "24.04";
      };

      ubuntuImage = pkgs.dockerTools.buildImage {
        name = "ubuntu-nix";
        tag = "latest";
        fromImage = ubuntuBase;
        copyToRoot = pkgs.buildEnv {
          name = "nix-tools";
          paths = [linuxPkgs.busybox];
        };
        config.Cmd = ["/bin/sh"];
      };
    in {
      devShells.default = pkgs.mkShell {
        shellHook = ''
          echo "Loading Docker images..."

          if command -v podman &>/dev/null; then
            podman load < ${layeredImage}
            podman load < ${ubuntuImage}
          elif command -v docker &>/dev/null; then
            docker load < ${layeredImage}
            docker load < ${ubuntuImage}
          else
            echo "No container runtime found"
            exit 1
          fi

          echo "Images loaded: ${layeredImage.imageName}:${layeredImage.imageTag}, ${ubuntuImage.imageName}:${ubuntuImage.imageTag}"
        '';
      };
    });
}

How it works

The flake uses Determinate’s built-in Linux builder on macOS:

linuxPkgs = import pkgs.path {system = "x86_64-linux";};

This builds natively for Linux using the remote builder, which is fast and fully cached.

Cross-compilation Alternative

Alternatively, you can cross-compile using:

linuxPkgs = pkgs.pkgsCross.musl64;

Note: This approach has no binary cache, so first builds will be slower.

Resources