24 KiB
MiNix: Architecture & Reference
Vision
A minimal, non-systemd Linux system with Nix as the operating system layer. dinit for PID 1 and process supervision. NixOS module system for declarative configuration. Bubblewrap for service sandboxing. No Nix daemon.
Two core principles drive the architecture:
- dinit over runit — proper dependency DAG, parallel boot, native service types that map cleanly to systemd, readiness notification, cgroup placement.
- Daemon-less Nix — the system store is root-owned (single-user Nix), user stores are per-user overlay stores. No nix-daemon.
Flake Architecture
Two independent flakes. The distro flake is build tooling — it never leaks into the end machine. The machine flake lives on the device and drives both system rebuilds and user configuration.
DISTRO FLAKE (git.axiomania.org/lebowski/MiNix)
Build tooling only — produces images, exports lib + modules.
Never baked into end machines.
inputs:
nixpkgs-stable github:NixOS/nixpkgs/nixos-25.11
nixpkgs-unstable github:NixOS/nixpkgs/nixos-unstable
home-manager github:nix-community/home-manager/release-25.11
outputs:
lib.minixSystem eval function for machine flakes
homeModules.default hm-dinit.nix (dinit user service module)
packages.*.vm disk image (dev only)
devShells.* qemu, debug tools
MACHINE FLAKE (/etc/minix/ on end device)
Per-machine config. Mutable — installed on first boot, user edits persist.
References distro via git+https:// (resolved by narHash, no path: fetcher).
inputs:
minix git+https://git.axiomania.org/lebowski/MiNix.git
nixpkgs-stable follows minix/nixpkgs-stable
nixpkgs-unstable follows minix/nixpkgs-unstable
outputs:
packages.*.system systemProfile (minix.lib.minixSystem + ./configuration.nix)
USER HM FLAKE (~/.config/home-manager/ per user)
Fully independent from machine flake. Auto-scaffolded on first boot.
User owns and edits these files.
inputs:
minix git+https://git.axiomania.org/lebowski/MiNix.git
nixpkgs follows minix/nixpkgs-stable
home-manager follows minix/home-manager
outputs:
homeConfigurations.<user> HM config (./home.nix)
Input flow (follows chain)
git+https://git.axiomania.org/lebowski/MiNix.git
|
distro flake
/ | \
nixpkgs-stable nixpkgs-unstable home-manager
| | |
"follows" "follows" "follows"
| | |
machine flake (/etc/minix/) user flake (~/.config/home-manager/)
One fetch (distro flake) pins everything. Machine and user flakes
have zero independent inputs — all shared via follows.
Why git+https:// (not path:)
The path: flake fetcher triggers builtins.path { filter; } which copies
store paths. In a local-overlay-store, this creates a SYMLINK in the upper
layer instead of a real directory. Nix then rejects it: "path is a symlink".
git+https:// inputs are locked by narHash. The overlay store resolves
narHashes by reading the lower store's DB — if the system build already
fetched the input, it's found without any copy. No symlink, no bug.
On-device layout
/etc/minix/ mutable (first-boot copy, edits persist)
flake.nix machine flake (git+https://...MiNix)
flake.lock locked pins
configuration.nix system config
hardware-configuration.nix hardware-specific
~/.config/home-manager/ per-user, auto-scaffolded on first boot
flake.nix user flake (git+https://...MiNix)
flake.lock locked pins
home.nix HM config
Who evaluates what
ROOT (minix-rebuild build):
nix build /etc/minix#packages.<arch>.system
-> fetches minix from Forgejo (by narHash)
-> minix.lib.minixSystem { modules = [ ./configuration.nix ] }
-> systemProfile -> nix-env --set -> reboot
USER (home-manager switch):
home-manager switch --flake ~/.config/home-manager
-> fetches minix from Forgejo (narHash hit — already in lower store)
-> homeConfigurations.<user> -> builds in overlay store -> activates
Architecture Overview
Layer 2: User Environment (Home Manager, per-user overlay store)
Per-user, declarative. Owns $HOME only.
Packages, dotfiles, user dinit services.
Overlay store: ~/.local/share/nix/ upper on /nix/store lower.
Updated: home-manager switch (live, no reboot)
Layer 1: System Profile (root-owned Nix store, single-user Nix)
/nix/var/nix/profiles/system
init — dinit init wrapper (generated)
kernel/ — vmlinuz, modules
etc/ — /etc files (built by NixOS etc.nix)
dinit.d/ — dinit service files
sw/ — system PATH (buildEnv)
firmware/ — hardware firmware
sysusers-credentials/ — hashed passwords for systemd-sysusers
Updated: minix-rebuild (root, single-user nix build) + reboot
Layer 0: Boot (NixOS initrd)
NixOS scripted initrd -> switch_root -> $profile/init -> dinit
Kernel cmdline: init=$profile/init
Two-layer model
System profile: "What does the machine need to run?" -> kernel, /etc, services
User environment: "What does the user see and use?" -> $HOME only
No separate "base rootfs" layer. The init wrapper and all boot logic are
generated as Nix derivations and live inside the system profile. The system
profile is self-contained — switch profiles by pointing
/nix/var/nix/profiles/system at a new store path and rebooting.
Per-user overlay stores
/nix/store (system) — root-owned, single-user Nix mode
minix-rebuild runs nix build as root
read-only bind mount at runtime (minix.hardenStore)
= overlay "lower" layer
~/.local/share/nix/ (user) — per-user, daemon-less
upper/ overlayfs upper layer (new packages go here)
work/ overlayfs work directory
merged/nix/store/ overlayfs mount point
var/nix/ overlay store state (db, profiles, gcroots)
Mount namespace setup (user-init at boot):
unshare --mountcreates a new mount namespace- Bind-mount ns handle to
/run/user/$UID/ns/mnt nsentersets up overlayfs: lower=/nix/store, upper=~/.local/share/nix/upper- Bind-mount merged store over /nix/store within the namespace
pam_minix_ns.soenters the namespace at login viasetns()
Store DB permissions: /nix/var/nix/db/ group-writable by users so overlay
store can read the lower store's sqlite DB (narHash lookups).
No nix-daemon. No nix-daemon.socket. No nixbld build users.
dinit service supervision
PID 1: dinit --system
+-- early-* services (scripted: pseudofs, eudev, modules, hostname, etc.)
+-- system services (translated from systemd.services.*)
+-- user dinit instances (spawned by turnstile at login)
+-- user services (from Home Manager / user store)
dinit provides dependency-ordered parallel startup, readiness notification,
native service types (process, bgprocess, scripted, internal, triggered),
and template services via @ suffix with $1 substitution.
Module Evaluation
system/eval.nix imports the full NixOS module-list.nix and filters via
a blacklist, then adds MiNix modules:
Blacklisted patterns (hasInfix match):
"system/boot/systemd" -> systemd-compat.nix (optionsOnly)
"system/boot/stage-2.nix" -> systemd-compat.nix (optionsOnly)
"system/activation/" -> systemd-compat.nix (optionsOnly)
"tasks/network-interfaces-systemd.nix" -> networkd backend, needs systemd
"system/boot/networkd.nix" -> same
"system/boot/shutdown.nix" -> unconditionally refs systemd
"services/system/nix-daemon" -> systemd-compat.nix (optionsOnly; no daemon)
Cherry-picked back from blanket-excluded dirs:
system/activation/activation-script.nix -> activation script infrastructure
system/boot/systemd/tmpfiles.nix -> tmpfiles.d generation
Uses evalModulesMinimal (not evalModules) for a lighter evaluation path.
The optionsOnly pattern
systemd-compat.nix uses optionsOnly to import stock NixOS modules keeping
only their options and imports — dropping config so no systemd wiring
fires. This gives us 1:1 API match with stock NixOS and zero maintenance
when upstream adds or changes options:
optionsOnly = path:
let mod = (import path) args;
in { options = mod.options or {}; imports = mod.imports or []; };
Applied to ~25 systemd sub-modules (journald, logind, user, coredump, oomd,
nspawn, sysupdate, sysusers, homed, userdbd, initrd, stage-2, networkd,
top-level, bootspec, activatable-system, switchable-system, nixos-init, etc.)
plus nix-daemon.nix (suppresses daemon service + nixbld users while keeping
nix.enable, nix.package etc. that config/nix.nix depends on).
Boot Flow
NixOS initrd -> switch_root -> $PROFILE/init (minix-init)
-> mount proc, sys, dev, run (fallbacks)
-> remount / rw
-> busybox --install -s /bin
-> exec dinit --system --services-dir $PROFILE/dinit.d
3-layer boot DAG
boot (internal)
+-- depends-on: system
+-- waits-for: serial-getty@ttyS0, autovt@tty1 (login last)
system (internal)
+-- depends-on: early
+-- waits-for: sshd, dhcpcd, firewall, ... (all auto-start services)
early (internal)
+-- depends-on: early-sysinit, early-hostname, early-loopback
early-sysinit (scripted)
+-- depends-on: early-etc, early-eudev, early-modules
tmpfiles --create --remove
sysusers (create/merge users, groups, passwords)
machine-id, sysctl, fstab mount
seedrng (entropy credit)
nix store registration
store hardening (optional)
activation scripts
early-etc (scripted) — populate /etc from profile
+-- depends-on: early-pseudofs
early-eudev (scripted) — udevd + coldplug + settle
+-- depends-on: early-pseudofs
early-modules (scripted) — link kernel modules, load modules-load.d
+-- depends-on: early-pseudofs, early-etc
early-hostname (scripted)
+-- depends-on: early-etc
early-loopback (scripted) — bring up lo
+-- depends-on: early-pseudofs
early-pseudofs (scripted) — mount tmp, devpts, shm, cgroup2, /run/wrappers
Getty templates have dependsMs = [ "system" ] so login prompts appear
after all service status lines.
Session management (turnstile)
turnstile (Chimera Linux project) provides session tracking and per-user dinit instance spawning:
Login flow:
user logs in via getty/sshd
-> PAM calls pam_turnstile.so (session rule)
-> pam_minix_ns.so enters user's mount namespace (overlay store)
-> turnstiled spawns dinit --user for that UID
-> XDG_RUNTIME_DIR created at /run/user/$UID
-> user dinit reads ~/.config/dinit.d/ + /etc/dinit.d/user/
-> PAM returns -> shell prompt
Logout flow:
last session for UID closes
-> turnstiled stops user dinit instance
-> XDG_RUNTIME_DIR cleaned up
/etc population (early-etc)
- Symlinks for immutable files (store paths)
- Copies for mutable files (those with
.modesidecar) - Mutable files preserved across reboots (only written on first boot)
- Stale symlinks from previous profiles cleaned up
- passwd/shadow/group/gshadow NOT in /etc profile — managed by sysusers at runtime
The systemd->dinit Translator
systemd-compat.nix is the core translation layer. It reads
systemd.services.* set by stock NixOS modules and generates dinit service
files. The optionsOnly pattern (importing NixOS modules with only their
option declarations) remains unchanged.
systemd -> dinit mapping
| systemd directive | dinit equivalent |
|---|---|
ExecStart |
command = |
ExecStop |
stop-command = |
Type=simple |
type = process |
Type=oneshot |
type = scripted |
Type=forking |
type = bgprocess + pid-file = |
Type=notify |
type = process (readiness not translated) |
User, Group |
run-as = |
Environment |
env-file = (generated) |
EnvironmentFile |
env-file += |
WorkingDirectory |
working-dir = |
Restart=always |
restart = yes |
Restart=on-failure |
restart = on-failure |
Restart=no |
restart = no |
After= + Requires= |
depends-on: (hard dep) |
After= + Wants= |
waits-for: (soft dep) |
After= (ordering only) |
after: |
LimitNOFILE |
rlimit-nofile = |
LimitCORE |
rlimit-core = |
LimitDATA |
rlimit-data = |
LimitAS |
rlimit-addrspace = |
AmbientCapabilities |
capabilities = (IAB ^CAP_* format) |
NoNewPrivileges |
options: no-new-privs |
PIDFile |
pid-file = (for bgprocess) |
StateDirectory etc. |
pre-start scripted setup |
Template services (@) |
native $1 substitution |
WantedBy= (empty) |
service not depended on by system (dormant) |
| Unix socket activation | socket-listen = + permissions/uid/gid |
Sandboxing (bubblewrap)
| systemd directive | bwrap equivalent |
|---|---|
ProtectSystem=strict |
--ro-bind / / + writable /var, /run, /tmp + declared dirs |
ProtectSystem=full |
--ro-bind /usr /usr --ro-bind /boot /boot --ro-bind /etc /etc |
ProtectSystem=true |
--ro-bind /usr /usr --ro-bind /boot /boot |
ProtectHome=true |
--tmpfs /home --tmpfs /root --tmpfs /run/user |
ProtectHome=read-only |
--ro-bind /home /home --ro-bind /root /root |
PrivateTmp=true |
--tmpfs /tmp --tmpfs /var/tmp |
PrivateDevices=true |
--dev /dev |
PrivateNetwork=true |
--unshare-net |
ProtectKernelTunables |
--ro-bind /proc/sys /proc/sys |
ProtectControlGroups |
--ro-bind /sys/fs/cgroup /sys/fs/cgroup |
ProtectHostname |
--unshare-uts |
NoNewPrivileges |
--new-session (+ native no-new-privs option) |
ReadOnlyPaths |
--ro-bind per path |
ReadWritePaths |
--bind per path |
Services with AmbientCapabilities skip bwrap and setpriv — bwrap can't
restore caps after uid change. These use dinit native capabilities instead.
Timer -> snooze translation
| systemd timerConfig | snooze flag |
|---|---|
OnCalendar |
-d, -m, -w, -H, -M, -S (parsed from calendar spec) |
RandomizedDelaySec |
-R <seconds> |
Persistent=true |
-t /var/lib/snooze/<name>.timefile |
AccuracySec |
-s <seconds> (slack) |
OnBootSec |
builtins.throw (not supported) |
OnUnitActiveSec |
builtins.throw (not supported) |
What can't be translated
| Feature | Status with dinit |
|---|---|
| Socket activation (full) | dinit has socket-listen for Unix sockets; TCP/UDP not supported |
DynamicUser |
Needs ephemeral UID allocator |
sd_notify protocol |
dinit has ready-notification (pipefd/pipevar) — different protocol |
Cgroup resource limits (CPUQuota, MemoryMax) |
dinit has run-in-cgroup; resource limits need cgroup controller config |
Monotonic timers (OnBootSec, etc.) |
Hard error — calendar scheduling only |
User/Group Management
users.nix provides minix.users and minix.groups options.
systemd-sysusers (standalone)
Users and groups are created at boot by systemd-sysusers (standalone build
from upstream systemd). The sysusers config is generated at build time:
/etc/sysusers.d/00-minix.conf:
u root 0:0 "System administrator" /root /bin/sh
u sshd 806:803 "SSH privilege separation user" /var/empty /bin/sh
g root 0
g wheel 1
m root wheel
Hashed passwords are passed via $CREDENTIALS_DIRECTORY. On first boot,
sysusers creates all entries. On subsequent boots, existing entries are
preserved (merge semantics) — password changes via passwd persist.
user-init service
The user-init dinit service runs at boot (after early) and:
- Creates home directories + sets ownership
- Fixes store DB permissions (group-writable for overlay store access)
- Creates build log directory permissions
- Per interactive user (uid >= 1000):
- Creates overlay store directories (~/.local/share/nix/)
- Writes ~/.config/nix/nix.conf (local-overlay-store config, first-boot only)
- Scaffolds ~/.config/home-manager/{flake.nix,home.nix} (first-boot only)
- Sets up mount namespace with overlay store (non-fatal)
Bridge from stock NixOS API
Translates users.users -> minix.users and users.groups -> minix.groups
with resolved UIDs/GIDs, including extraGroups membership bridging.
Standalone Tools
| Tool | Source | Purpose |
|---|---|---|
systemd-tmpfiles |
systemd v260.1 standalone build | tmpfiles.d directory/symlink creation |
systemd-sysusers |
systemd v260.1 standalone build | User/group creation with merge semantics |
seedrng |
busybox built-in | RNG seeding with BLAKE2s mixing + entropy credit |
virt-what |
nixpkgs (48 KiB) | Virtualization detection (CPUID-level) |
turnstile |
chimera-linux/turnstile v0.1.11 | Session tracking + per-user dinit spawning |
hwdb.bin |
systemd v260.1 hwdb.d + eudev udevadm | Hardware identification database |
pam_minix_ns.so |
system/packages/pam_minix_ns.c | PAM module — enters user mount namespace at login |
System Commands
| Command | User | Purpose |
|---|---|---|
minix-rebuild build [FLAKE] |
root | Build system profile from /etc/minix/ and switch to it |
minix-rebuild switch PATH |
root | Switch to a pre-built system profile |
minix-generate-config |
root | Probe hardware, generate configuration.nix |
home-manager switch --flake ~/.config/home-manager |
user | Build and activate HM generation |
NixOS Compatibility
What works (standard NixOS options)
boot.kernelPackages,boot.kernelModules,boot.kernelParamsboot.initrd.availableKernelModules,boot.initrd.kernelModulesfileSystems.*,swapDeviceshardware.firmware,hardware.enableRedistributableFirmwareservices.udev.extraRules,services.udev.packagesenvironment.etc.*,environment.systemPackagesnixpkgs.overlays,nixpkgs.config,nixpkgs.hostPlatformsecurity.pam.services.*(stock NixOS PAM, with SHA-512 enforced)networking.firewall.*(via translated iptables service)systemd.services.*(auto-translated to dinit)systemd.timers.*,startAt(translated to snooze)systemd.tmpfiles.*(standalone tmpfiles binary)system.stateVersion
A standard NixOS hardware-configuration.nix works as-is in MiNix.
MiNix-specific options
| Option | Purpose |
|---|---|
minix.users.* / minix.groups.* |
sysusers-based user/group management |
minix.hostname |
Hostname (decoupled from networkd) |
minix.hardenStore |
Read-only bind mount for /nix/store |
minix.turnstile.* |
Session tracking config |
minix.flakeRef |
Default flake ref for minix-rebuild (default: /etc/minix) |
minix.machineFlake |
Path to machine flake dir (installed to /etc/minix/) |
dinit.services.* |
Native dinit service definitions |
users.users and users.groups from stock NixOS modules are bridged
automatically to minix.users/minix.groups — both APIs work.
File Structure
flake.nix Distro flake: minixSystem, homeModules, packages
reference.md This document
system/
eval.nix Module evaluator (blacklist + evalModulesMinimal)
overlay.nix nixpkgs overlay (strip systemd deps, enable dinit caps)
lib/
translator.nix systemd->dinit service translator
modules/
systemd-compat.nix optionsOnly imports + translator wiring
dinit-services.nix dinit.services -> environment.etc."dinit.d/..."
dinit-init.nix Init wrapper + 3-layer boot DAG + shutdown hook
users.nix minix.users/groups + sysusers + overlay store + HM scaffold
eudev-compat.nix eudev-as-systemd wrapper + standalone tools + initrd fixes
toplevel.nix Assembles system profile derivation
generate-config.nix minix-generate-config (virt-what + stock perl script)
base-etc.nix minix.hostname, LOCALE_ARCHIVE override
defaults.nix Feature flags, distro identity, overlay, syslogd
rebuild.nix minix-rebuild (build + switch)
turnstile.nix turnstile session tracking + PAM integration
hm-dinit.nix Home Manager module for dinit user services
system-path.nix Busybox-based core packages
packages/
systemd-tmpfiles-standalone.nix Standalone tmpfiles from systemd v260.1
systemd-sysusers-standalone.nix Standalone sysusers from systemd v260.1
turnstile.nix turnstile v0.1.11 (session/login tracker)
pam-minix-ns.nix PAM module for mount namespace entry
pam_minix_ns.c PAM module C source
machines/
qemu-vm/
flake.nix Machine flake (git+https://...MiNix, system only)
configuration.nix QEMU VM config (getty, sshd, dhcpcd, etc.)
hardware-configuration.nix Detected hardware (kernel modules, filesystems)
image/
disk.nix Persistent qcow2 disk image + QEMU runner
Refs/
nixpkgs/ Shallow clone for module inspection
dinit/ dinit source + man pages
dinit-chimera/ Chimera Linux's dinit integration
NixNG/modules/dinit/ NixNG's Nix-native dinit module
turnstile/ turnstile source (Chimera Linux)
Known Optimization Opportunities
nixpkgs source duplication (~444 MB)
The nixpkgs source tree is not part of the system closure (only package
outputs are). When a user evaluates their HM flake, nix fetches and unpacks
the full nixpkgs source into the overlay upper layer. Fix: add nixpkgs source
to system closure via system.extraDependencies so it's in the lower store.
glibc-locales duplication (~222 MB)
The systemd-stripping overlay changes the derivation graph enough to produce a different glibc-locales hash than "clean" nixpkgs. Users get a second copy. Fix: investigate overlay impact on locale derivation chain.
.drv file accumulation (~1280 files)
Nix evaluation instantiates all referenced derivations in the store. In the overlay store, these accumulate in the upper layer. Normal nix behavior — visible because overlay makes the user's additions explicit.
Development Status
Done
- System profile via evalModulesMinimal + full NixOS module set
- systemd->dinit translator with bwrap sandboxing
- Timer->snooze translation (calendar timers, RandomizedDelaySec, Persistent)
- AmbientCapabilities -> dinit native IAB capabilities
- Socket activation for Unix sockets
- Template services (getty@, serial-getty@, autovt@) with native dinit $1
- User/group management with sysusers + dynamic allocation + NixOS bridge
- eudev compatibility layer + standalone tools (tmpfiles, sysusers)
- Persistent qcow2 disk image builder
- minix-generate-config and minix-rebuild tools
- 3-layer boot DAG (early->system->boot) + shutdown hook
- Serial console detection via kernel params
- Mutable /etc with first-boot-only writes + sysusers merge semantics
- nix-daemon suppressed (no daemon, no nixbld users)
- turnstile session tracking + PAM integration
- hwdb.bin + systemd-cat stub
- Per-user overlay stores (local-overlay-store, mount namespace, pam_minix_ns.so)
- Flake architecture: distro flake (build tooling) + machine flake (/etc/minix/)
- Home Manager integration (user flake auto-scaffolded, follows distro pins)
- VM-tested: full pipeline — boot, overlay store, nix run, home-manager switch
Future
- Optimize: preload nixpkgs/HM source into system closure
- Optimize: fix glibc-locales derivation divergence from overlay
- System-provided user services (D-Bus session bus, PipeWire)
- Board support (Rockchip SBCs, SD card images)
- ISO for generic x86 install
- Live system switching without reboot