nixos configuration
so. doll is finally a nixer. oh boy. you wanna know what would make this even worse? if doll literated this bitch up!!!!!!!!!!!! ayyyyyyyyyyyyyyyyyyyyyyyyyyyyy
Table of Contents
Goals
Documentation
the whole entire reason doll is doing a literate configuration in the first place is to increase the ratio of documentation vs code.
a big problem with writing code is that eventually you get to a point where you look back at your code and go "wait. what the fuck. why did i do that. why did i do any of this." now imagine having that problem with the code that runs your entire computer. that. would be bad.
thus, in making our nixos config literate, we literally write down paragraphs about all of our decisions so that in the nebulous near future in which we come back to our config not knowing what the hell we were doing, we can look back on those paragraphs and remember what the hell we were doing. which, of course, was witchcraft. it was always witchcraft.
Minimalism
the benefit of having to lay out every single program that you are installing is that it is very easy to know all of the programs you have installed… and think about whether they really need to be there.
doll notices that over time, as it uses its computer, it gets into a habit of installing a bunch of programs in order to solve some sort of issue (whether technical, practical,
or fantastical) and then never going back through to remove said programs that are left over in the aftermath. this innevitably creates bloat, and since it usually runs machines
that are limited in storage (both this machine nyxtrix and its previous machine crimson were only 256 gb,,,,), that bloat can add up fast.
the declarative process of nixos means that in order to change the programs one has installed, they are required to see all of the applications they have installed. every time one changes their system, the consequences of their actions are laid out in front of them, and one always has the opportunity to look upon the march of progress and make judgement calls on what new daemons are no longer of use within their COMP. and doll thinks that's pretty neat.
Hardware
this is the automatically generated hardware configuration from nixos-generate-config.
It doesn't contain much. Most of the real goodies are in a dependency doll will talk
about later.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "usb_storage" "sd_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
Main Configuration
this is the main configuration file for nixos. later on, doll will try to split this up into multiple files, but for now, just one is fine.
# ========== configuration.nix - main configuration - © 2026 millicent ==========
Design
currently, our nixos config is firmly in channel land. as a beginner to nixos, it is
recommended that we start with channels and then move our way to flakes
once we are comfortable with channels. thus, our config is currently in
/etc/nixos/configuration.nix and not ~/nixos/flake.nix for example.
Beginning Boilerplate & Imports
{ config, lib, pkgs, ... }:
{
imports =
[ # Include tweaks from NixOS-Hardware.
<nixos-hardware/lenovo/thinkpad/t480s>
# Include the results of the hardware scan.
./hardware-configuration.nix
];
Boot
# Use the systemd-boot EFI boot loader.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
# Use ZFS filesystem
boot.supportedFilesystems = [ "zfs" ];
boot.initrd.systemd.enable = true;
boot.zfs.forceImportRoot = false;
Filesystem
# Configure filesystem paths
## Root
fileSystems."/" = {
device = "door/root";
fsType = "zfs";
};
## Nix Configuration
fileSystems."/nix" = {
device = "door/nix";
fsType = "zfs";
};
## Var
fileSystems."/var" = {
device = "door/var";
fsType = "zfs";
};
## Home
fileSystems."/home" = {
device = "door/home";
fsType = "zfs";
};
## Boot Partition
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/C605-9D58"; # UUID for /dev/sda1
fsType = "vfat";
options = [ "fmask=0077" "dmask=0077" ];
};
## ZFS shenanigans
## - TODO huh???
services.zfs = {
autoScrub.enable = true;
trim.enable = true;
};
Swap
# Swap
# - ZFS does not like swap partitions very much. We have made one /just/ in case, but we
# will mostly use ZRAM for swap instead.
## Swap Partition
swapDevices = [
{
device = "dev/disk/by-partuuid/b974352a-22c7-4601-85da-bf6df7feb7d7"; # UUID for /dev/sda3
randomEncryption.enable = true; # no swap skimming today, buckeroo
}
];
## ZRAM
zramSwap = {
enable = true;
algorithm = "zstd";
};
Networking
# Networking
networking = {
hostId = "3b86203e"; # head -c 8 /etc/machine-id
hostName = "nyxtrix";
nftables.enable = true;
firewall.enable = false;
networkmanager.enable = true;
};
Core System
Settings
# System Settings
system.stateVersion = "26.05"; # latest stable as of 2026-08-20
time.timeZone = "America/Chicago"; # eh close enough
console.keyMap = "us";
i18n.defaultLocale = "en_US.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "en_US.UTF-8";
LC_IDENTIFICATION = "en_US.UTF-8";
LC_MEASUREMENT = "en_US.UTF-8";
LC_MONETARY = "en_US.UTF-8";
LC_NAME = "en_US.UTF-8";
LC_NUMERIC = "en_US.UTF-8";
LC_PAPER = "en_US.UTF-8";
LC_TELEPHONE = "en_US.UTF-8";
LC_TIME = "en_US.UTF-8";
};
Services
# System Services
services = {
libinput.enable = true; # libinput is based
fwupd.enable = true; # LVFS firmware updates
printing.enable = true; # CUPS!
flatpak.enable = true; # unfortunately, flatpak is based
tailscale.enable = true; # unfortunately, tailscale is based
power-profiles-daemon.enable = true; # performance & power saving profiles
upower.enable = true; # being able to see battery status
## OpenSSH
## - basically, allow SSH'ing into this, but REQUIRE use of a key
openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = "no";
};
};
};
Graphical Environment
# Graphical Environment
## - awwwwww you know what it isssssss
## wayland niri wayland niri wayland niri wayland niri
services.xserver = {
enable = true; # BUT IT SAYS XSE- ssshhhhhhhhh
## keyboard language
xkb = {
layout = "us";
variant = "";
};
## silly: just. yeet x11. only wayland.
excludePackages = [ pkgs.xterm ];
};
## niri
programs.niri.enable = true;
## display manager: greetd (for now)
services.greetd = {
enable = true;
settings = {
default_session = {
command = "${config.programs.niri.package}/bin/niri-session";
user = "millicent";
};
};
};
## By default, NixOS strips out part of the path given to the niri.service unit.
## That's silly. We're gonna let niri have the whole path because it's a good girl
systemd.user.services.niri.enableDefaultPath = false;
## for defaults sake, we are installing polkit, gnome-keyring, and alacritty.
## we will edit this later, don't worry
security.polkit.enable = true;
services.gnome.gnome-keyring.enable = true;
## TODO add systemPackages [ ... alacritty ... ]
Sound
insert joke here.
# Sound
services.pulseaudio.enable = false; # FUC U
security.rtkit.enable = true;
services.pipewire = { # YEAAAAA
enable = true;
# systemWide = true; # so other apps can see that it's working
alsa.enable = true; # for maximum compatability
alsa.support32Bit = true; # "
pulse.enable = true; # pipewire-pulse it think
jack.enable = true; # pipewire-jack it think 2i
# wireplumber.enable = true;
};
Packages
# Packages
## enabling unstable
nixpkgs.config = {
allowUnfree = true;
packageOverrides = pkgs: {
unstable = import <nixos-unstable> {
config = config.nixpkgs.config;
};
};
permittedInsecurePackages = [ "olm-3.2.16" ];
};
System Packages
## system packages
environment.systemPackages = with pkgs; [
<<pkgs-duhs>>
<<pkgs-gstreamer>>
<<pkgs-languages>>
<<pkgs-shell-de>>
<<pkgs-extras>>
];
- the duhs
## the duhs glibc coreutils zstd flock zip unzip wget lm_sensors # for additional hw support whois dig # forget what this is for, think DNS? gnupg # unfortunately, GPG is still used sometimes ffmpeg wireguard-tools reaction # dependency for wireguard-tools git doas # sudo but better olm - gstreamer
## gstreamer gst_all_1.gstreamer # base package gst_all_1.gst-plugins-base # base set of plugins gst_all_1.gst-plugins-good # well-supported plugin set gst_all_1.gst-libav # use ffmpeg wherever possible gst_all_1.gst-vaapi # use hardware acceleration - programming languages
## programming languages python314 # python python314Packages.pip gcc # c/++ gdb valgrind go # go cargo # rust rustc rust-analyzer gradle # jdk jdk25 nodejs # js/ts vitejs typescript-language-server # js/ts LSP vscode-langservers-extracted # css/html/json LSPs nixd # nix LSP yaml-language-server # yaml LSP claude-code # oh no...- Rust LSP:
rust-analyzer
vscode's rust-analyzer extension ships its own copy of the language server, pulled from the marketplace as a generic linux binary. on most distros that just works. on nixos it does not: there is no real
/lib64/ld-linux-x86-64.so.2here (ours is a stub that exists purely to print a polite error), so the bundled server cannot start at all.the symptom that sent me looking was sneakier than a crash, and it turned out not to be this problem at all. rust-analyzer was reporting a phantom
E0308in a rust project – insisting&[&str; 1]would not coerce to&[&str]– on exactly one of seven identical call sites, whilecargo check --all-targetsstayed perfectly clean. a language server that had quietly died and was still serving diagnostics it had lying around from earlier is a tempting explanation for that, and it is the one i reached for first.it was wrong.
pgrep -af rust-analyzerturned up a server very much alive, running the extension's other copy out of~/.config/Code/User/globalStorage/, with tworust-analyzer-proc-macro-srvprocesses hanging off our own rustc. the bundled binary is broken; the downloaded one is not. what had actually gone stale was the server's file map:ps -o lstart=put its start two and a half hours before the file's last write, and somewhere in that window the file had been renamed. it was still analyzing the old buffer, where that line number pointed at entirely different code – which is also why one call site lit up and the other six did not. restarting the server cleared it.so the loader problem above is real, but it is not the only thing that produces phantom diagnostics, and from inside the editor the two look identical.
pgreptells them apart in a second: if a server is already running, you want a restart, not a rebuild. that also makes the package below insurance rather than a rescue – nothing was broken at the time, but the extension re-downloads that copy on its own schedule, and there is no promise the next one runs.installing the server from nixpkgs gives us a binary linked against the things that actually exist on this machine. the extension then has to be told to use it, and that setting lives outside nix in
~/.config/Code/User/settings.json:"rust-analyzer.server.path": "rust-analyzer"the alternative was
programs.nix-ld.enable, which supplies a real loader so generic binaries can run. that would fix this and every future case like it in one line, but it also means running an unpatched foreign binary when a properly built one was right there. bigger hammer than the job needs.(this section was written by claude opus 5 on 2026-09-01 – millie, it is not in your voice, reword me!)
… actually, I don't think I will :::3c
rust-analyzer - JS LSP:
typescript-language-server
eglot ships with a server list for every language it knows about, so most of the time you install the binary and it just works. typescript is not one of those times, and the reason is worth writing down because the error message is baffling on its own:
[eglot] (warning) None of 'rass ts, typescript-language-server' are valid executables
rass tsis not a program, and it is not a garbled copy-paste either – it is sitting in emacs 31.1's own copy of eglot, atlisp/progmodes/eglot.el:271:Generated by Claude | claude-opus-5 | 2026-09-25 15:04 UTC
. ,(eglot-alternatives '(("rass ts") ("typescript-language-server" "--stdio")))that first entry should say
("vtsls" "--stdio"). something mangled it upstream, which means the two-candidate fallback is really a one-candidate fallback, and if you do not havetypescript-language-serveryou get an error naming a program that cannot exist.vtslsis packaged in nixpkgs too, but reaching it would need an expliciteglot-server-programsentry to route around the broken line, so the boring option wins.vscode-langservers-extractedcovers the rest of the web stack. one package, four binaries –vscode-{css,html,json,eslint}-language-server– under exactly the names eglot already looks for, so nothing needs configuring. worth knowing that.astrofiles do not need them:astro-lshandles css and typescript embedded inside a component, so these only earn their keep on standalone.css,.htmland.jsonfiles.one trap, and it is the same trap
astro-lsfalls into.typescript-language-serverdoes its real work through tsserver, which it loads from the javascript typescript api in a project'snode_modules. typescript 7 is the native go rewrite and ships no such api – notypescript.js, notsserverlibrary.js, nothing for the server to load. so a project that innocently runsnpm install --save-dev typescriptnow pulls 7.x and the language server dies on it. pin the 6.x line per project (astro 7 builds against^6.0.3) and both servers stay happy.typescript-language-server # js/ts LSP vscode-langservers-extracted # css/html/json LSPs - Nix LSP:
nixd
eglot knows three nix servers and tries them in this order:
nil,rnix-lsp,nixd. that ordering matters less than the fact that it is a list –eglot-alternativesonly picks silently when exactly one candidate is onPATH. install two and every nix buffer greets you with "more than one server executable available" and makes you choose. so this is a pick-one situation, not a belt-and-braces one.nixdgets the nod because it evaluates with nix's own libraries rather than reimplementing them, which means package andlibcompletion come from a realimport <nixpkgs> { }– its default, and correct as-is on a channels setup like this one.nilis lighter and faster to start; if this machine ever feels the ~200-300mb nixpkgs takes just to hold the attribute names, that is the trade to revisit.completing option names –
services.pipewire.enableand friends, i.e. most of what this very file is made of – needs telling nixd where the option set lives. that is emacs-side configuration, not nix-side, so it belongs in the eglot block rather than here:Generated by Claude | claude-opus-5 | 2026-09-25 15:31 UTC
(add-to-list 'eglot-server-programs '(nix-mode . ("nixd" :initializationOptions (:nixd (:options (:nixos (:expr "(import <nixpkgs/nixos> {}).options")))))))treat that
expras a starting point rather than gospel – the right expression depends on how the config is entered, and the flake form looks nothing like the channels form. the nixd configuration docs have both. everything is evaluated lazily, so a wrong expression fails at completion time instead of at startup, which makes it harder to notice than you would like.yaml-language-serverneeds none of this. eglot has a plain entry for it, no alternatives to disambiguate and no options to pass, so installing it is the whole job.nixd # nix LSP
- Rust LSP:
- shell & DE
## shell & DE xwayland-satellite # for x11 app compatability unstable.noctalia # chill shell kdePackages.dolphin # file manager pwvucontrol # for audio management foot # terminal emulator - extras
## extras chromium # for handshake specifically librewolf # LET THE BROWSER. BE TRANS!!!!! neovim # vim. but slightly cooler vesktop # discord. but slightly cooler vscode # boooooooo vscode-extensions.anthropic.claude-code signal-desktop #mom,,, mixxx # untzuntzuntzuntzuntz
Fonts
## fonts!
fonts.packages = with pkgs; [
atkinson-hyperlegible-next # sans
ioskeley-mono.normal-NF # mono
drafting-mono # fancy mono
];
Emacs
## emacs
## - the default emacs given by stable is a fairly outdated version.
## - thus, we are using a community overlay to grab the latest
## - tagged version and make sure it is the pure GTK version for wayland.
services.emacs = {
enable = true;
defaultEditor = true;
package = pkgs.emacs-unstable-pgtk;
};
nixpkgs.overlays = [
(import (builtins.fetchTarball {
url = "https://github.com/nix-community/emacs-overlay/archive/master.tar.gz";
}))
];
Docker
## docker (rootless)
virtualisation.docker = {
# Consider disabling the system wide Docker daemon
enable = false;
rootless = {
enable = true;
setSocketVariable = true;
# Optionally customize rootless Docker daemon settings
daemon.settings = {
dns = [ "1.1.1.1" "8.8.8.8" ];
registry-mirrors = [ "https://mirror.gcr.io" ];
};
};
};
Flakes?
# NixOS Flakes
# - for now, we are keeping this /off/, just so we can get used to
# - configuring our system the nix way. once we are more settled in,
# - then we will enable flakes.
nix.settings.experimental-features = [ "nix-command" "flakes" ];
User Account
# User Account
users.users.millicent = {
isNormalUser = true;
description = "Millicent Kasanova";
extraGroups = [ "networkmanager" "wheel" "docker" ];
};
}