NixOS on Raspberry Pi

Published March 26, 2021
Photo by Jordane Mathieu on Unsplash

A couple of weeks back I was reintroduced to NixOS and it sparked my interest. Their headline is “Reproducible builds and deployments.” and that’s something we all strive for, right?

I started by installing Nix on my workstation but couldn’t really get a feel for it. Instead, I remembered I had a Raspberry Pi 3B laying around. This seemed like a perfect device to tinker on with NixOS.

Searching for other people doing the same as I was planning to do was hard. It seems like the NixOS community is small when it comes to Raspberry Pi’s.

I did find a couple of blog posts, but NixOS is a rapid changing distribution so those were almost all out of date.

The Raspberry Pi has it’s own bootloader, nothing like GRUB that’s on a default Ubuntu installation or systemd-boot that’s on an Arch installation. This makes the booting process in NixOS a bit harder.

I settled on the following configuration to boot NixOS on my Raspberry Pi:

# Disable GRUB
boot.loader.grub.enable = false;

# Enable Raspberry PI bootloader
boot.loader.raspberryPi.enable = true;
boot.loader.raspberryPi.version = 3;
boot.loader.raspberryPi.uboot.enable = true; 

When first using NixOS it’s hard to get used to. Everything is so different compared to a “normal” Linux installation. But when you get the grasp of it, it all makes sense. Our code is declarative, so why should our operating system not be?

For the networking I prefer to use the built-in Wi-Fi chip. Less cables means more happiness.

I also install Avahi to be able to access the Raspberry Pi via the hostname.

# Enable Avahi for service discovery
services.avahi = {
    enable = true;
    publish = {
        enable = true;
        addresses = true;
        workstation = true;
    };
};

networking = {
    # Set hostname so it can be reached via raspberry-nix.local
    hostName = "raspberry-nix";

    # Set up wireless chip
    wireless.enable = true;
    wireless.interfaces = [ "wlan0" ];

    # Configure my network, read the password from /secret/my-ssid-password
    wireless.networks."My SSID".psk = (lib.fileContents "/secret/my-ssid-password");

    # Set DNS nameservers
    nameservers = [ "1.1.1.1" "8.8.8.8" ];

    extraHosts = ''
        127.0.0.1 raspberry-nix.local
    '';

    # Firewall opens SSH and HTTP
    firewall.allowedTCPPorts = [ 22 80 ];
};

I chose to install Gitea on the Raspberry Pi for a self hosted Git server.

One feature of the Nix configuration format that I really like is that you can include other files. I use this to include the external services.

imports = [
    ./services/mariadb.nix
    ./services/gitea.nix
    ./services/caddy.nix
]; 

Let’s start at the top of the service list. The MariaDB service has the following contents:

{ lib, pkgs, ... }: 

let
  password = (lib.fileContents "/secret/gitea_db");
in {
  services.mysql = {
    enable = true;
    package = pkgs.mariadb; 
    initialDatabases = [
      {
        name = "gitea";
        schema = pkgs.writeText "gitea.sql" ''
          create user if not exists gitea@'localhost' identified by ${password};
          grant all privileges on gitea.* to gitea@'localhost' identified by ${password};
        ''; 
      }
    ];
  };
}

A couple of interesting things in this service configuration are the lib.fileContents and the pkgs.writeText

The lib.fileContents gets the contents of the file stored in that location and assigns it to the variable password.

pkgs.writeText writes arbitrary contents to a file, in this case it’s used for a temporary file to create the initial database schema for Gitea.

The Gitea service file has the following contents:

{ config, ... }:

{
  services.gitea = {
    enable = true;
    user = "git";
    domain = "raspberry-nix.local";
    rootUrl = "http://raspberry-nix.local/gitea/";
    httpAddress = "0.0.0.0";
    database = {
      type = "mysql";
      user = "git";
      name = "gitea";
      passwordFile = "/secret/gitea_db";
    };
    appName = "Gitea Bouma.tech";
  };
  
  users.users.git = {
    description = "Gitea Service";
    isNormalUser = true;
    home = config.services.gitea.stateDir;
    createHome = true;
    useDefaultShell = true;
  }; 
}

For exposing the Gitea service to the outside world, I use Caddy. The configuration file for Caddy is this:

{ ... }:

{
  services.caddy = {
    enable = true;
    config = ''
      http://raspberry-nix.local {
         route /gitea/* {
          uri strip_prefix /gitea
          reverse_proxy localhost:3000
        }
      }
    '';
  };
}

For completeness sake, I also uploaded my configuration.nix file to a GitHub gist: https://gist.github.com/anned20/2ef3e7488805eaf2403aa6b3b409908d