Koala

A NixOS Spring Festival Journey

Over the Spring Festival holiday, I came across ryan4yin’s blog and found NixOS really intriguing. After tinkering with Linux for so long, I was sick of editing config files under /etc and kept running into dependency conflicts. Every system upgrade would break my environment, and Docker was only a band-aid fix.

What Drew Me to NixOS

  • Define your entire system with a single /etc/nixos/configuration.nix file
  • Every configuration update creates a Generation, so you can always roll back from a bad change
  • Fully reproducible development environments - just git clone, no more pacman or apt

The Foundation of Reproducibility

In NixOS, the /nix/store directory is read-only. It stores all packages, libraries, and configuration files installed through the Nix package manager. Each package has a unique hash-based path, such as /nix/store/l9i65cfgnxgrxlghxg792146w00kafcn-rocksdb-9.8.4/.

This read-only design ensures packages can’t be arbitrarily modified, different versions of the same package can coexist, and if a system update goes wrong, you can roll back instantly. This is the key to NixOS achieving full reproducibility.

Installing NixOS

You can download the Minimal ISO image from the official NixOS website to install on a fresh machine. Alternatively, you can use the nixos-infect script to convert a non-NixOS system into NixOS with one command. The latter has been tested and works on Alibaba Cloud ECS.

The configuration file /etc/nixos/configuration.nix is incredibly straightforward. In the configuration below, we specify the root user’s authorizedKeys and password, set up the SSH server, configure vscode-server, and define the hostname - all from this single file. Once you’ve written the file, just run nixos-rebuild switch and everything is set up.

For my server, I manually partitioned the disk, mounted btrfs, and then ran the nixos-install script. For a more detailed installation guide, check out this blog post: Building Your Dream OS from Scratch. If you’re not entirely confident with the process, you can try it in a virtual machine first. When you’re ready to deploy to a server, simply copy the configuration file over and run nixos-rebuild switch - everything will be identical.

# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).

{ config, lib, pkgs, ... }:

{
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
      # vscode server
      (fetchTarball {
        url = "https://github.com/nix-community/nixos-vscode-server/tarball/master";
        sha256 = "09j4kvsxw1d5dvnhbsgih0icbrxqv90nzf0b589rb5z6gnzwjnqf";
      })
    ];

  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;

  # Pick only one of the below networking options.
  # networking.wireless.enable = true;
  networking.networkmanager.enable = true;

  # Time zone and locale settings
  time.timeZone = "Asia/Shanghai";
  i18n.defaultLocale = "en_US.UTF-8";
  networking.hostName = "hostname";

  nixpkgs.config.allowUnfree = true;

  # Installed packages
  environment.systemPackages = with pkgs; [
    openssl
    wget
    curl
    nano
    tailscale
  ];
  # Set default editor
  environment.variables = {
    EDITOR = "nano";
  };

  # OpenSSH and Tailscale configuration
  services.openssh.settings = {
    PermitRootLogin = "yes";
    PasswordAuthentication = true;
  };

  # Enable system services, similar to systemctl enable --now xxx
  services.openssh.enable=true;
  services.tailscale.enable = true;
  services.vscode-server.enable = true;

  # Root user configuration
  users.users.root = {
    openssh.authorizedKeys.keys = [
      "ssh-rsa AAAAB3NzaC......tTMFP5y8v8= therainisme@qq.com"
    ];
    # Random password I set
    password = "4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865";
  };

  system.stateVersion = "24.11";
}

Development Environment Setup (Devbox and Direnv)

In software development, environment configuration has always been a headache. Different projects have varying requirements for compiler versions, dependency libraries, and environment variables, often leading to the classic “works on my machine” problem. This section introduces Devbox and Direnv for managing development environments, using two typical Go projects as examples to demonstrate how to achieve precise control over your dev environment.

Truth be told, these two tools work even better on non-NixOS systems than they do on NixOS itself.

Prerequisites

Before all the examples, you must install devbox and direnv, and add a direnv plugin to your IDE. If you’re using NixOS, add the following configuration and run nixos-rebuild switch:

programs.direnv.enable = true;
environment.systemPackages = with pkgs; [
  ...
  devbox
];

Example 1: A Go Program with CGO Enabled, Linked Against RocksDB v9.8.4

You need to compile a Go program that uses CGO and links against RocksDB v9.8.4. The complexity of this scenario lies in:

  • CGO support must be enabled
  • A specific version of a C library (RocksDB) is required
  • pkg-config is needed to locate header file paths

First, navigate to the project’s root directory and run:

# Initialize Devbox project
devbox init

# Add pinned version dependencies
devbox add rocksdb@9.8.4   # Specify RocksDB version; defaults to @latest if omitted
devbox add pkg-config
devbox add gcc

# Generate Direnv configuration
devbox generate direnv

Now open a shell and run pkg-config --cflags --libs rocksdb - everything is already configured. It’s that simple.

$ pkg-config --cflags --libs rocksdb
-I/nix/store/l9i65cfgnxgrxlghxg792146w00kafcn-rocksdb-9.8.4/include -L/nix/store/l9i65cfgnxgrxlghxg792146w00kafcn-rocksdb-9.8.4/lib -lrocksdb

Example 2: A Legacy Go Program That Requires Go 1.20.3

First, navigate to the project’s root directory and run:

devbox init
devbox add go@1.20.3  # Pin the exact Go version
devbox generate direnv

Now open a shell and verify with these steps:

# Verify within the project directory
$ go version
go version go1.20.3 linux/amd64

# Check environment isolation
$ which go
/home/user/.devbox/nix/profile/default/bin/go

More Miscellaneous Configuration

TODO

Nixpkg Resources

  1. https://search.nixos.org/options - Search for configuration options in /etc/nixos/configuration.nix. For example, to install Docker, according to its description, you only need to add one line: virtualisation.docker.enable = true;
  2. https://search.nixos.org/packages - Search nixpkgs. Unless the package is extremely obscure, you’ll find it here. However, older versions of packages are not available - for instance, Go 1.20 can’t be found in the 24.11 channel.
  3. https://www.nixhub.io/ - Devbox’s package search website, where you can find both the latest packages and all historical versions. For example, Go 1.20 is available here.

Common Devbox Commands

  • devbox init: Initialize devbox for a project. After running, a devbox.json file appears in the project directory. You can either edit the file directly to add packages or use devbox add, similar to package managers like npm.
  • devbox add: Add a package. You can specify a version with @xxx, e.g. devbox add rocksdb@9.8.4.
  • dev search: Search for packages
  • dev rm: Remove a package
  • devbox generate direnv: Automatically generate an .envrc file for direnv. Once set up, the environment loads automatically.
  • devbox shell: Manually enter the environment shell
  • devbox run: Manually run a command (add scripts under the shell.scripts field in devbox.json, similar to the scripts field in Node’s package.json)