Multi-OS config for moha, managed with Nix flakes. One repo, four hosts:
| Host | OS | Manages | Flake output |
|---|---|---|---|
nixos-btw |
NixOS | system + user | nixosConfigurations.nixos-btw |
macos |
macOS (M1, 2020) | system + user | darwinConfigurations.macos |
arch-btw |
Arch Linux (any distro) | user only | homeConfigurations."moha@arch-btw" |
ubuntu-btw |
Ubuntu/Debian, headless | user only | homeConfigurations."moha@ubuntu-btw" |
ubuntu-btw-gui |
Ubuntu/Debian, with display | user only | homeConfigurations."moha@ubuntu-btw-gui" |
All of them share the same home/home.nix (packages, shell, symlinked app configs).
Only the system layer differs per OS: hosts/nixos-btw/configuration.nix (NixOS)
and hosts/macos/darwin-configuration.nix (nix-darwin). Arch and Ubuntu have no
system module — they aren't NixOS, so only home-manager (user-level) applies there.
Every standalone (non-NixOS) host is emitted twice, once per architecture:
moha@<host> is x86_64-linux and moha@<host>-aarch64 is aarch64-linux.
Two names are needed because homeConfigurations is a plain attrset and a
flake can't read the builder's architecture in pure eval
(builtins.currentSystem is off limits). Nothing has to be typed by hand
though — Taskfile.yml and scripts/bootstrap-ubuntu.sh both derive the
suffix from uname -m, so task ubuntu:switch picks the right output on an
Intel VM and on an ARM one alike.
home/home.nix takes a gui flag (passed from flake.nix via
extraSpecialArgs, default true). Setting it to false — as
ubuntu-btw does — drops Xorg, oxwm, ghostty, firefox, gparted,
rofi and the fonts, and skips the Mesa LD_LIBRARY_PATH workaround. On a
box with no display those are a few hundred MB that can never be used, and
the LD_LIBRARY_PATH one is worse than useless there: it's inherited by
every process in the login session, where it can shadow the distro's own
glibc/libstdc++ for system binaries.
dotfiles/
├── flake.nix # every host output lives here
├── home/home.nix # shared: packages, shell, xdg symlinks
├── hosts/
│ ├── nixos-btw/configuration.nix # NixOS system config
│ │ └── hardware-configuration.nix # machine-specific, generated by nixos-generate-config
│ └── macos/darwin-configuration.nix # nix-darwin system config
├── config/ # app configs, symlinked into ~/.config by home.nix
│ ├── nvim/
│ └── oxwm/ # Linux only, skipped on Darwin
├── scripts/
│ └── bootstrap-ubuntu.sh # one-shot Ubuntu setup, see below
└── Taskfile.yml # `task <name>` shortcuts, see below
home/home.nix symlinks config/* into ~/.config/* via
mkOutOfStoreSymlink, pointing at ~/dotfiles/config/.... The repo must be
cloned to exactly ~/dotfiles on every host, or the symlinks break.
Install Nix first (all platforms), with flakes enabled:
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- installOr, if go-task is already available on the machine (e.g. installed via
pacman/brew before Nix): task nix:install runs the same command. On
Ubuntu/Debian, skip this — scripts/bootstrap-ubuntu.sh installs Nix itself,
after the apt packages the installer depends on.
On macOS, install Xcode Command Line Tools before that: xcode-select --install.
go-task is used to shorten the commands below.
It's optional — the raw nix/nixos-rebuild/darwin-rebuild/home-manager
commands work directly too. Install with nix profile install nixpkgs#go-task
or via your package manager.
git clone <this-repo-url> ~/dotfiles
cd ~/dotfiles
task --list # see everything below, with descriptionstask nixos:switch # apply now
task nixos:boot # apply on next boot only
task nixos:test # apply for this session, revert on rebootIncludes KVM/QEMU virtualization (virtualisation.libvirtd, virt-manager,
SPICE USB redirection). moha is in the libvirtd/kvm groups — log out
and back in (or newgrp libvirtd) after the first switch for group
membership to take effect, then launch virt-manager to create VMs.
First run on a fresh machine — create a user account named exactly moha
during the macOS setup assistant, since home.username and
users.users.moha.home are hardcoded to that name. Install Homebrew itself
manually before the first switch — nix-darwin manages the casks below, not
the brew binary:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"Then:
task macos:bootstrap # first ever switch, before darwin-rebuild is on PATH
task macos:switch # every switch after thatHomebrew casks: hosts/macos/darwin-configuration.nix installs ghostty
and karabiner-elements via Homebrew instead of nixpkgs. ghostty has no
aarch64-darwin build in nixpkgs at all. karabiner-elements does build, but
it's only the Settings UI — missing the DriverKit system extension and
launchd daemons that do the actual key remapping (and that make it show up
under System Settings → Login Items & Extensions → Allow in the Background).
The cask runs the real upstream .pkg installer instead, which sets both up
correctly.
Docker: macOS has no Linux kernel for dockerd to run on, so
home/home.nix installs colima (a lightweight Linux VM) alongside
docker-client (CLI only). Start the VM once per boot, then use docker
exactly as on Linux:
colima startNo system module: this only manages packages and dotfiles for moha, via
standalone home-manager. Works on any non-NixOS Linux distro with Nix
installed, not just Arch.
task arch:bootstrap # first ever switch
task arch:switch # every switch after thatBoth run with -b backup: standalone home-manager has no
backupFileExtension option (that's only available through the
NixOS/nix-darwin integration modules), so pre-existing dotfiles
(~/.bashrc, etc.) would otherwise make the switch fail on conflict —
they get renamed to *.backup instead.
There's no display manager on plain Arch by default. home/home.nix
installs xorg-server + xinit from Nix (no pacman needed) along with a
~/.xinitrc that execs oxwm, and auto-runs startx on tty1 login via
~/.bash_profile/~/.zprofile — so logging in on tty1 drops straight
into the GUI. Running oxwm directly from a TTY instead of through
startx fails with CannotOpenDisplay — it needs a running X server first.
One-time manual step required: Nix's xorg-server ships with no input
driver, so the keyboard/mouse get enumerated but never actually respond
(X logs No input driver specified, ignoring this device for each one —
easy to mistake for a full freeze, since everything else, like oxwm's
status bar, keeps rendering fine). home/home.nix installs the matching
xf86-input-libinput driver and points ~/.xserverrc at it via
-modulepath (not root-restricted), but the InputClass rule that tells
Xorg to actually use that driver has to live under
/etc/X11/xorg.conf.d — -configdir only accepts absolute paths as
root, so this one file can't be home-manager-managed:
sudo mkdir -p /etc/X11/xorg.conf.d
sudo tee /etc/X11/xorg.conf.d/40-libinput.conf > /dev/null <<'EOF'
Section "InputClass"
Identifier "libinput pointer catchall"
MatchIsPointer "on"
MatchDevicePath "/dev/input/event*"
Driver "libinput"
EndSection
Section "InputClass"
Identifier "libinput keyboard catchall"
MatchIsKeyboard "on"
MatchDevicePath "/dev/input/event*"
Driver "libinput"
EndSection
EOFFor a real display manager instead of the tty1-autostart hack, install
ly (via pacman) — home/home.nix skips the autostart trick once it
detects one, either at /etc/ly/config.ini or as an enabled
/etc/systemd/system/display-manager.service (which is how gdm3, sddm and
lightdm register themselves):
sudo pacman -S ly
sudo systemctl enable ly.service
sudo systemctl disable getty@tty1.serviceDocker: standalone home-manager only manages moha's user packages —
it can't enable a system-level systemd service, so dockerd has to come
from pacman instead (unlike NixOS, which gets it via
virtualisation.docker.enable in hosts/nixos-btw/configuration.nix):
sudo pacman -S docker
sudo systemctl enable --now docker.service
sudo usermod -aG docker moha # log out and back in (or `newgrp docker`) afterSame standalone home-manager setup as Arch, with a bootstrap script that also
handles the two things Arch's instructions leave to pacman: the handful of
apt packages Nix's own installer needs, and the system services home-manager
can't touch. One command on a brand-new VM, on either architecture:
git clone https://github.com/moha-bekh/dotfiles ~/dotfiles
~/dotfiles/scripts/bootstrap-ubuntu.sh # headless VM
~/dotfiles/scripts/bootstrap-ubuntu.sh --gui --docker --zshOr, with nothing cloned yet:
curl -fsSL https://raw.githubusercontent.com/moha-bekh/dotfiles/main/scripts/bootstrap-ubuntu.sh | bashIt is idempotent — every step checks whether it already ran — so re-running it after a failure or a change of flags is fine. In order, it:
- refuses to continue as
rootor as a user other thanmoha(both are hardcoded inhome/home.nix, and failing here beats failing halfway through a switch); - resolves the flake target from
uname -mand the--guiflag, e.g..#moha@ubuntu-btw-aarch64; apt installs only what Nix itself needs first:curl,xz-utils(not in a minimal Ubuntu image, and the installer downloads a.tar.xz),ca-certificates,git. Everything else deliberately comes from Nix — that's what keeps the hosts identical;- clones the repo to
~/dotfilesif it isn't there; - removes a
~/.config→dotfiles/configsymlink if one exists (see below); - installs Nix via the Determinate Systems installer if absent, then sources
nix-daemon.sh— the installer only patches login shell files, sonixis not onPATHin the already-running shell; - runs the home-manager switch;
- with
--docker, installsdocker.io+docker-compose-v2from apt, enables the service and adds you to thedockergroup. This can't come from the flake: standalone home-manager managesmoha's user packages only and cannot enable a system-level systemd unit — same reason Arch gets Docker from pacman, while NixOS gets it fromvirtualisation.docker.enable; - with
--gui, writes/etc/X11/xorg.conf.d/40-libinput.conf— the one root-owned file behind the Arch section's "one-time manual step", so on Ubuntu it isn't manual; - with
--zsh, adds~/.nix-profile/bin/zshto/etc/shells(chshrejects anything absent from it) and makes it the login shell.
Then, for every update after that:
task ubuntu:switch # headless
task ubuntu:switch:gui # desktopDon't symlink ~/.config to config/ wholesale. It looks like a shortcut
to the same result and isn't: every application on the machine then gets write
access to the repo (so ~/.config/foo/state.json shows up in git status), the
macOS- and Arch-only configs become visible on Ubuntu too, home-manager writes
its symlinks into the repo, and nothing installs the packages those configs
drive. The flake links each app directory individually via
mkOutOfStoreSymlink instead, which is why the switch is what gives you the
full config and not just the files.
Ubuntu Desktop already has a display manager (gdm3), so --gui does not
add the tty1 startx autostart that Arch gets — home/home.nix detects the
enabled display-manager.service and skips it. To actually land in oxwm
rather than GNOME, pick it from the session list at the login screen; that
needs a .desktop session file under /usr/share/xsessions, which is
root-owned and therefore outside this repo. The startx-on-tty1 path works
unchanged on Ubuntu Server.
task check # nix flake check --no-build — evaluates the NixOS/darwin outputs
task update # bump nixpkgs / home-manager / nix-darwin, re-lockRun task check after editing home/home.nix or any host file, before
switching — it catches eval errors without building anything. Note that it
only covers the flake's standard outputs: homeConfigurations isn't one of
them, so the standalone hosts aren't checked by it. Evaluate those explicitly:
nix eval .#homeConfigurations --apply builtins.attrNames # list every name
for h in ubuntu-btw ubuntu-btw-aarch64 ubuntu-btw-gui-aarch64; do
nix eval --raw ".#homeConfigurations.\"moha@$h\".activationPackage.drvPath"
donesystem.stateVersion(NixOS) andsystem.stateVersion(nix-darwin) are set once at first install and should never be bumped afterward — they pin on-disk data format compatibility, not the Nixpkgs version in use.- The
oxwmandpicomconfig symlinks only apply on Linux (lib.optionalAttrs pkgs.stdenv.isLinuxinhome/home.nix) — macOS has no X11 window manager. The packages are gated more tightly still, ongui && isLinux, so the headlessubuntu-btwhost gets neither. - Language servers come from Nix, not
mason.nvim.home/home.nixinstallsclang-tools(clangd),gopls,zls,vtsls,rust-analyzerandnil, andconfig/nvim/lua/plugins/lsp.luamarks each onemason = falseplus sets mason'sPATH = "append"so those win over any copy mason already downloaded. Two things force this: mason has no aarch64-linuxclangdat all (The current platform is unsupported.— so C/C++ has no LSP on an ARM VM), and mason's prebuilt binaries are linked against/lib64/ld-linux, which doesn't exist on NixOS. Servers nixpkgs isn't asked for here (lua_ls,marksman,terraform-ls, the ansible/docker ones) still come from mason. rust-analyzeris driven by rustaceanvim, not nvim-lspconfig — LazyVim'slang.rustextra setsrust_analyzer = { enabled = false }on purpose and rustaceanvim picks the binary up from$PATH. That's why it isn't in themason = falselist.zlsreports parse errors but not type errors out of the box: semantic Zig diagnostics needenable_build_on_save, which makes zls runzig buildon every save. Hover, completion and go-to-definition work regardless.config/git/.gitconfigsetsinteractive.diffFilter = delta, sodeltais in the shared package list — without itgit add -pfails on any host that didn't happen to have it from pacman/brew.unzipis there for the same reason: LazyVim'smason.nvimshells out to it to unpack LSP servers.