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:
| Aggregate | Scope |
|---|---|
generic.base | cross-platform system config |
darwin.base | nix-darwin system + home-manager wiring |
nixos.base | NixOS system + home-manager wiring |
homeManager.base | cross-platform home config |
homeManager.darwin / homeManager.nixos | platform-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 buildLayeredImageubuntu-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
Linux Builder (Recommended)
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.