Happiness is a full punnet

Below is my minimal NixOS configuration for a Raspberry Pi 4B. It includes everything needed, ready to be loaded from a generic flake.nix.

First enable binfmt on your build machine:

boot.binfmt.emulatedSystems = [ "aarch64-linux" ];

Then build the SD card image with:

nix build '.#nixosConfigurations.YOUR_HOSTNAME.config.system.build.sdImage'

…and rebuild remotely with:

nixos-rebuild switch --flake .#YOUR_HOSTNAME --target-host RASPI_IP --use-remote-sudo

Below is the configuration.nix, with notes on how this all works after the fold:

{ lib, pkgs, modulesPath, ... }:
{
  imports = [
    # This module installs the firmware
    "${modulesPath}/installer/sd-card/sd-image-aarch64.nix"
  ];

  nix.settings = {
    # This is needed to allow building remotely
    trusted-users = [ "YOUR_USERNAME" ];

    # The nix-community cache has aarch64 builds of unfree packages,
    # which aren't in the normal cache
    substituters = [
      "https://nix-community.cachix.org"
    ];
    trusted-public-keys = [
      "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
    ];
  };

  nixpkgs = {
    hostPlatform = "aarch64-linux";
    config = {
      allowUnfree = true;
    };
  };

  # These options make the sd card image build faster
  boot.supportedFilesystems.zfs = lib.mkForce false;
  sdImage.compressImage = false;

  networking = {
    # Set your hostname
    hostName = "YOUR_HOSTNAME";
    useNetworkd = true;
  };

  # Replace networkd with NetworkManager at your discretion
  systemd = {
    network = {
      enable = true;

      networks."10-lan" = {
        # This is the correct interface name on my raspi 4b
        matchConfig.Name = "end0";

        networkConfig.DHCP = "yes";
        linkConfig.RequiredForOnline = "routable";
      };
    };
  };

  # Add your username and ssh key
  users.users.YOUR_USERNAME = {
    isNormalUser = true;
    extraGroups = [ "wheel" ];
    openssh.authorizedKeys.keys = [ "YOUR_SSH_PUBLIC_KEY" ];
  };

  # Our user doesn't have a password, so we let them
  # do sudo without one
  security.sudo.wheelNeedsPassword = false;

  services = {
    openssh.enable = true;
  };

  # Set your timezone
  time.timeZone = "YOUR_TIMEZONE";

  environment.systemPackages = with pkgs; [
    libraspberrypi
    raspberrypi-eeprom
  ];

  hardware.enableRedistributableFirmware = true;

  system.stateVersion = "24.11";
}

Some notes:

  • It’s completely stock, not using the raspberry pi module from nixos-hardware or any special vendor kernel.

    This works just fine for me. Note however that I’m running it totally headless, I have no idea if the GPU or anything else is functional. Theoretically everything is supposed to just work though. I would suggest applying directives from the above module à la carte to fix specific problems you might have.

  • Despite the generic name, the sd-image-aarch64 module is what actually sets up the raspi firmware. It provides this build target:

    nix build '.#nixosConfigurations.YOUR_HOSTNAME.config.system.build.sdImage'
    

    which will output a result symlink to a directory with an .img file.

  • That SD image will include the usual firmware partition, indeed the raspi hardware requires this to boot. However it won’t be automatically mounted in NixOS, nor will it interact with the nix store in any way. The same sd-image-aarch64 module also defines the filesystems, and it helpfully does include an entry for the firmware partition, just with auto-mount disabled. You can mount it like so:

sudo mkdir -p /boot/firmware
sudo mount /boot/firmware

and then edit config.txt normally. If you want to configure the firmware from nix, have a look at the raspberry-pi-nix flake. I haven’t tried it but it looks very swish.

  • You probably don’t want to do your rebuilds on the raspi itself, as it’ll be slow, simply because the raspi is underpowered. Also it’s boring. You want to build everything on a fast machine and push it over.

    In aid of this, nixos-rebuild provides the --target-host option. The full incantation is like so:

    nixos-rebuild switch --flake .#YOUR_HOSTNAME --target-host RASPI_IP --use-remote-sudo
    
  • The issue of course is that you are likely building on an x86 machine, whereas the raspi is aarch64. There are two solutions to this, cross-compilation and emulation. In common with other posters I have had no luck with cross compiling. CPU emulation works fine however. Simply set this on your build machine:

    boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
    

    and the kernel’s binfmt feature will transparently invoke qemu to do all the building. This, as you might imagine, is slow, but it’s still faster than doing everything on the pi off an SD card assuming you have a fast workstation.

  • The good news though is that the nixpkgs cache features aarch64 builds, so you mostly won’t need to compile anything at all. The emulation is still required so that the right hashes can be calculated and the cache addressed correctly in the first place, but it’s all basically instantaneous.

  • Problems arise when installing unfree packages, which aren’t provided in the cache. Notably this includes the unifi-controller management tool for ubiquiti networking equipment, along with its main dependency, the dreaded mongodb. Compiling the whole of mongo under qemu takes a very long time indeed. I gave up and just used a docker image - I suggest you do the same. Even better, get a microtik AP instead.

  • The nix-community cache does valiant work to provide builds of unfree packages from nixpkgs, including mongo etc, but at the time of writing they only have 1TB of storage and so artefacts don’t stay in there for very long before they’re pushed out. Maybe you will have better luck - I have included the relevant config.

  • If you’re using a macbook, this should all just work without any special fiddling as you’re natively on aarch64. Fingers crossed.