How I manage my Guix System configs

by Jonathan Frederickson — Sun 15 February 2026

Tags: guix

I've been meaning to write up a post on how I manage my Guix System configurations for a while, because I've hit on a solution that feels kinda nice, inspired by how folks do things in NixOS.

In the NixOS world, there's a tool called nixos-generate-config that automatically creates both a configuration.nix and a hardware-configuration.nix for you. The idea is that the hardware-specific bits (like your partition layout, initrd modules, and such) go into hardware-configuration.nix, and the rest of your configuration goes into configuration.nix. We don't have an equivalent of nixos-generate-config in the Guix world to help automate this, but the general pattern seemed pretty nice, so I've roughly replicated it for my Guix config.

It ends up feeling a little messier in Guix than in Nix, because of some differences in how we build up our configs. But it's not too bad. There's a couple different layers here, so let's start with my config.scm for one of my machines. I'm omitting a few things for brevity but you can see the full file contents here if you're curious.

(define-module (system machines wired config)
  ...
  #:use-module (system machines wired hardware-configuration))

(define-public wired-operating-system
  (operating-system
   (inherit hardware-configuration)
   (locale "en_US.utf8")
   (timezone "America/New_York")
   (host-name "wired")
   (users
    (cons*
     (user-account
       ...)
     (operating-system-users hardware-configuration)))
   (packages
    (append
     (list luanti-server
           minetest-game
           ...)
     (operating-system-packages hardware-configuration)))))

This defines the operating-system that you would actually use in, for example, a guix system reconfigure. A few things to note about this:

Okay, let's take a look at hardware-configuration:

(define-module (system machines wired hardware-configuration)
  #:use-module (gnu)
  #:use-module (nongnu packages linux)
  #:use-module (system machines server-base))

(define-public hardware-configuration
  (operating-system
   (inherit jfred-server-base-system)
   (kernel linux)
   (firmware (list linux-firmware))
   (keyboard-layout (keyboard-layout "us"))

   (bootloader (bootloader-configuration
                (bootloader grub-efi-bootloader)
                (targets (list "/boot/efi"))
                (keyboard-layout keyboard-layout)))

   (mapped-devices (list (mapped-device
                          (source (uuid
                                   "a1775b70-cfc4-44d5-9fc3-0ddaff9eb694"))
                          (target "cryptroot")
                          (type luks-device-mapping))))

   (file-systems (cons* (file-system
                         (mount-point "/boot/efi")
                         (device (uuid "AF86-B19B"
                                       'fat32))
                         (type "vfat"))
                       (file-system
                         (mount-point "/")
                         (device "/dev/mapper/cryptroot")
                         (type "ext4")
                         (dependencies mapped-devices)) %base-file-systems))

   (host-name #f)))

This is where all my disk layout stuff lives, and it's where I keep any hardware-specific quirks. For example, this machine is an Intel NUC, so I need some proprietary firmware for all the hardware to work properly. So I use linux-firmware and the linux kernel from Nonguix here.

How exactly to split up the config between files is a bit of a judgement call, because not everything feels like it cleanly falls into one camp or the other. I moved things around a few times as I was putting this together.

You might have also noticed that the hardware-configuration itself inherits from another config (jfred-server-base-system). Yeah, I have multiple layers of inheritance here! Here's that definition:

(define-module (system machines server-base)
  ...
  #:use-module (system machines base))

(define-public jfred-server-base-system
  (operating-system
   ;; Set these in hardware-configuration.scm
   (bootloader #f)
   (host-name #f)
   (file-systems #f)
   (sudoers-file
     (plain-file "sudoers"
                 (string-append (plain-file-content %sudoers-specification)
                                (format #f "~a ALL = NOPASSWD: ALL~%"
                                        "jfred"))))

   ;; Base configs for my servers
   (locale "en_US.utf8")
   (keyboard-layout (keyboard-layout "us"))
   (timezone "America/New_York")
   (users (cons* (user-account
                  (name "jfred")
                  (comment "Jonathan Frederickson")
                  (group "users")
                  (home-directory "/home/jfred")
                  (supplementary-groups '("wheel" "netdev" "audio" "video")))
                 %base-user-accounts))
   (packages %base-packages)

   (services
   (append (list
            (service openssh-service-type
                     (openssh-configuration
                      (use-pam? #f)))
            (service elogind-service-type)
            (service dhcpcd-service-type)
            (service dbus-root-service-type)
            (service docker-service-type)
            (service containerd-service-type)
            (service ntp-service-type))
           (jfred-append-base-services %base-services)))))

This is where I keep configuration that should apply to all of my servers, rather than just one. I always want to have a user for myself in sudoers on each of them, for it to be in my local timezone, and for it to have the us keyboard layout. And there's a base set of services that I'll almost certainly want on any server I'm running.

This is the part of my config that feels the messiest to me. You might notice the fields set to #f at the top there; that's because it's an error to have an operating-system record that does not define all the fields. I'll always need to set the bootloader, filesystems, and hostname for any new machine anyway, so in practice it's not an issue - it just feels a bit weird.

And yes, there's yet another layer of composition with jfred-append-base-services at the end there. But I think this is where I'm going to stop. :)

I hope this post is helpful to someone in the future! And if you have any other ideas, I'd love to hear about them.


Comment: