How to make NixOS actually work on a Raspberry Pi 4 Jul 21, 2024 Jul 21, 2024
One day, I was on my Raspberry Pi and decided I should probably update Raspbian. However, I completely botched the job and irreparably destroyed the OS and everything I had so carefully orchestrated on it. I figured I might as well try out NixOS, since it’s abilities to conveniently configure anything and to easily roll back broken configurations should prevent anything like that from happening again.
It turned out to be not so easy. This article contains everything I had to figure out through hours of debugging so you don’t have to do the same. I’ll be talking about the RPI 4 since that’s what I have. Milage may vary on other models. First, here are some invaluable resources you will absolutely need:
Resources#
I owe a debt of gratitude to Carlos Vaz for this wonderful blog post about making a functional base installation of NixOS on a Raspberry Pi. My installation follows the guide, but using btrfs instead of zfs. I won’t cover base installation because that article covers it well.
Here’s a NixOS wiki article about the Raspberry PI 4.
This lets you easily search for Nix packages. In the top bar, you can switch to “NixOS options” which allows you to search for NixOS options. That’s super handy as well.
This is the compendium of options for Home Manager, which you absolutely want to use because it will allow you to configure lots of userspace programs within the NixOS configuration.
I strongly encourage you to use Agenix. It’s a super convenient tool for storing secrets in your Nix config using your SSH public and private keys for encryption. I was intimidated by it at first but it’s really not so bad.
Here is my NixOS config for my Raspberry Pi.
Help! My RPI needs to be exorcised!#
If your Raspberry Pi acts like it’s being posessed by a demon (randomly turning off, not booting for mysterious reasons), it’s most likely a power issue rather than a NixOS issue. It could also be a hardware failure, but let’s pray that it isn’t. A lightning bolt symbol in the corner is a smoking gun, but if it’s not there, that doesn’t necessarily mean it’s okay.
To solve this, first verify that your power supply is actually designed to power a Raspberry Pi. If you just found a plug out of your drawer, it’s probably giving however much voltage and power it wants instead of what the Raspberry Pi needs. Second, verify that whatever you have plugged into your RPI isn’t sucking too much power. If you have that problem, you can solve it by using a USB hub with a separate power supply or by using hardware that consumes less power.
BTRFS Backups#
When you install your system, it’s very important to make
sure root is mounted from a subvolume instead of the “root” of your
BTRFS partition. When root is on a subvolume, if you accidentally
rm -rf
something, you can simply delete your root subvolume
and mv
a snapshot in it’s place. You can’t do that if your
root isn’t on a subvolume, and that will turn into a huge headache for
you later.
Configuring BTRBK on NixOS#
Here’s where the cool parts of NixOS come in…
To configure BTRBK on NixOS, you first want to insert
pkgs.btrbk
into your
environment.systemPackages
.
Then, you need to choose a directory to make snapshots available in
(I’ll choose /btrbk-snapshots
). You’ll want to install a
systemd service that will create the folder if it doesn’t already exist.
You could create it by hand, but declaring the service in your config
would make reinstallation of your system seamless. The following
configuration will do the trick:
-snapshots = {
systemd.services.btrbkenable = true;
description = "Make the btrbk-snapshots directory if it doesn't exist";
wantedBy = [ "multi-user.target" ];
# Make sure to change the directory to the one you want
script = ''
if [ ! -d /btrbk-snapshots ]; then
mkdir /btrbk-snapshots
fi
'';
};
Third, you want a oneshot service that will execute the
btrbk
command when started:
"btrbk-snapshot" = {
systemd.services.script = ''
exec /run/current-system/sw/bin/btrbk -q run
'';
serviceConfig = {
Type = "oneshot";
User = "root";
};
};
Then, you want a systemd timer that will execute the oneshot service at boot as well as once every hour:
"btrbk-snapshot" = {
systemd.timers.wantedBy = ["timers.target"];
timerConfig = {
OnBootSec = "0m";
OnUnitActiveSec = "1h";
Unit = "btrbk-snapshot.service";
};
};
Finally, you can create your BTRBK config:
{
environment.etc = # Change the configuration as you please
"btrbk/btrbk.conf".text = ''
timestamp_format long
snapshot_preserve_min 16h
snapshot_preserve 48h 7d 3w 4m 1y
volume /
snapshot_dir btrbk-snapshots
subvolume .
'';
};
Why doesn’t GPIO work?#
Some GPIO libraries (at least the one I was using) will read
/proc/cpuinfo
for your Raspberry Pi’s hardware information.
The problem is that the Raspberry Pi kernel modifies
/proc/cpuinfo
to provide extra info, but NixOS by default
ships with the mainline linux kernel. To use the Raspberry Pi kernel
(and get better hardware support), you can import the RPI hardware
config from the NixOS
Hardware repo:
[
imports = # other imports
"${builtins.fetchGit { url = "https://github.com/NixOS/nixos-hardware.git"; }}/raspberry-pi/4"
]
If your library fails to mmap
/dev/mem
,
then you’ll need to include the kernel parameters
iomem=relaxed
and strict-devmem=0
because the
linux kernel disallows that by default:
[
boot.kernelParams = "iomem=relaxed"
"strict-devmem=0"
];
Why does it say I’m running out of storage when I rebuild even though my drive is not nearly full?#
This one’s an oopsie on my part. NixOS builds in the
/tmp
directory and I configured /tmp
to use
tmpfs
(which “stores” everything in memory). When I rebuild
anything large (like the linux kernel), it ran out of space because the
RPI doesn’t have much memory. To fix the problem, you can stop using
tmpfs
on /tmp
or you can configure a big
enough swap partition.
Why does changing kernel paramaters have no effect?#
When NixOS writes kernel parameters, it writes them into the
bootloader config. But for whatever reason, the kernel ignores the
parameters written there and instead uses whatever’s in
/boot/cmdline.txt
. Assuming you’re using
systemd-boot
(like the blog post I linked earlier), you can
use the following config to copy the parameters to the right spot:
-boot.extraFiles."cmdline.txt" = builtins.toFile "cmdline.txt" "${builtins.toString config.boot.kernelParams} init=/nix/var/nix/profiles/system/init"; boot.loader.systemd
Why are there no WiFi (or other) drivers?#
Something you can try is removing systemd-boot
and
enabling the generic bootloader:
-extlinux-compatible.enable = true; boot.loader.generic
I have no idea why this works, but for whatever reason using
systemd-boot
prevents some drivers from loading.
Unfortunately, this does prevent the previous workaround for fixing
kernel parameters from working since it relies on
systemd-boot
options. If you know what’s going on here,
please let me know.
That’s all folks!