thisago's blog

Guix/Nix in a Fedora Minimal TemplateVM

Table of Contents

Solene has a pretty straightforward guide to setup Nix on a AppVM without daemon. I recommend reading it first: Solene'% : How to install Nix in a Qubes OS AppVM

This guide covers the installation of the Guix/Nix daemon (as root) in a minimal Fedora TemplateVM, and the setup of bind-dirs for persistence of the derivations built from AppVMs.

Below steps covers the installation of Guix, and even below, the Nix steps, which you can append or replace. It works together too.

Steps for Installation

  1. [dom0] Create TemplateVM and connect the NetVM to it. In my case, called fedora-42-minimal-guix
  2. Setup passwordless root and networking agent
    1. [dom0] Open a root shell at the template

      qvm-run -u root fedora-42-minimal-guix xterm
      
    2. [TemplateVM] Then install on the template the passwordless and networking Qubes agent:

      dnf install -y qubes-core-agent-passwordless-root qubes-core-agent-networking
      

      You can now close your root terminal and open xterm from dom0 applications menu to get a user shell.

  3. [TemplateVM] Install hard dependencies for Guix installation

    sudo dnf install -y wget xz # needed for retrieving `latestArtifactUrl` in below script
    sudo dnf install -y xq
    
  4. [TemplateVM] Install latest Guix build by script (should be root)
    1. Install it using latest tarball and installation script

      latestArtifactUrl=$(curl -sL 'https://ci.guix.gnu.org/search/latest?query=spec:tarball+status:success+system:x86_64-linux+guix-binary.tar.xz' | xq -q 'ul.list-group.d-flex.flex-row > a' -a href | xargs -I{} echo 'https://ci.guix.gnu.org{}') &&
      cd /tmp &&
      wget -O guix-install.sh https://guix.gnu.org/install.sh &&
      wget -O guix-binary.tar.xz "$latestArtifactUrl" &&
      chmod +x guix-install.sh &&
      GUIX_BINARY_FILE_NAME=guix-binary.tar.xz ./guix-install.sh
      
    2. Update Guix Using Codeberg because git.guix.gnu is now down for me.

      guix pull --url=https://codeberg.org/guix/guix
      
  5. [TemplateVM] Setup the AppVM base home by linking the user profile.1

    sudo mkdir -p /etc/skel/.config/guix/
    sudo ln -s /var/guix/profiles/per-user/user/current-guix /etc/skel/.config/guix/current
    
  6. [dom0] Shutdown TemplateVM and remove NetVM from it.
  7. Configure bind-dirs for persistence at /gnu/store and /var/guix at AppVM. Since Guix Store is at /gnu/store and the profiles at /var/guix/profiles, we will persist both.
    1. [AppVM] Configure the directories for bind-dirs.

      sudo mkdir -p /rw/config/qubes-bind-dirs.d/
      echo "binds+=( '/gnu/store' '/var/guix' )" | sudo tee /rw/config/qubes-bind-dirs.d/50_user.conf
      
    2. Shutdown AppVM
    3. [dom0] Make sure to increase the AppVM private disk because the /gnu/store will now be persisted on it, which was previously on the TemplateVM.
      • Expect a slower startup time for the initial copy process.
      • For reference, my newly created AppVM had 960K of disk usage, now it has 2.5G.
    4. [dom0] Start AppVM back again.
    5. [AppVM] Optionally, to test the persistence, run a shell with a package, reboot and run again to expect no downloads to be made.

      guix shell hello
      

Done! Now you have a persisting AppVM with Guix.

Nix installation

Replace/append at Guix steps (referenced in parenthesis) with below steps, the other steps remains exactly the same, and required.

  1. (3) [TemplateVM] Install hard dependencies for Nix intallation.

    sudo dnf install -y xz
    
  2. (4) [TemplateVM] Install via Nix installation script

    sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install) --daemon
    
  3. (5) [TemplateVM] Setup the AppVM base home
    • Link the profile2

      sudo mkdir -p /etc/skel/.local/state/nix/
      sudo ln -s /nix/var/nix/profiles/default /etc/skel/.local/state/nix/profile
      # You might want to setup for TemplateVM user too
      mkdir -p ~/.local/state/nix/
      ln -s /nix/var/nix/profiles/default ~/.local/state/nix/profile
      
    • Source nix.sh in users' .bashrc:

      echo "test -f /home/user/.local/state/nix/profile/etc/profile.d/nix.sh && source /home/user/.local/state/nix/profile/etc/profile.d/nix.sh" | sudo tee -a /etc/skel/.bashrc
      
  4. (7.1) [AppVM] Configure bind-dirs for persistence at /nix. Nix persists all at /nix, so it's enough.

    sudo mkdir -p /rw/config/qubes-bind-dirs.d/
    echo "binds+=( '/nix' )" | sudo tee /rw/config/qubes-bind-dirs.d/50_user.conf
    # NOTE: If you're installing both together, instead append the '/nix' with the '/gnu/store' and '/var/guix'. Example:
    # echo "binds+=( '/gnu/store' '/var/guix' '/nix' )" | sudo tee /rw/config/qubes-bind-dirs.d/50_user.conf
    
  5. (7.5) [AppVM] Example package to perform the test:

    nix-shell -p hello
    

Nice! Now you have a persisting AppVM with Nix. (Or both!)

In my case, both Guix and Nix (with their hello packages) took 3.5G from my AppVM.

Pitfalls I Faced

  • Guix installation script fetches the tarball from a FTP mirror, and the latest release is from 12/2022.

    guix-binary-1.4.0.x86_64-linux.tar.xz 18-Dec-2022 21:16 102312024
    guix-binary-1.4.0.x86_64-linux.tar.xz.sig 18-Dec-2022 21:16 833
    

    And I got stuck in this already solved issue: #344 - {rootless guix-daemon} `guix build` error: read-only file system. was …

  • Guix stores the profiles (and its db) at /var/guix, so it need persistence too. And only persisting /var/guix/profiles/per-user/user won't work.
  • The qubes-mount-dirs.service has a timeout of 60s. I was unable to setup bind-dirs for Nix and Guix at same time on two tests. In my case, the Guix had worked but Nix got corrupted, because it was the last on my bind-dirs setup.

    The notifications I got:

    [user@dom0 Desktop]$ grep -B 3 -A 2 'Cannot connect to qrexec agent for 60 seconds' ~/.cache/xfce4/notifyd/log
    [2025-11-09T07:44:31.486420-05]
    app_name=org.qubes.qui.tray.Domains
    summary=Qube Status: ix-test-for-blog-again
    body=Qube ix-test-for-blog-again has failed to start: Cannot connect to qrexec agent for 60 seconds, see /var/log/xen/console/guest-ix-test-for-blog-again.log for details
    app_icon=dialog-warning
    expire-timeout=-1
    --
    [2025-11-09T07:53:16.712243-05]
    app_name=org.qubes.qui.tray.Domains
    summary=Qube Status: ix2
    body=Qube ix2 has failed to start: Cannot connect to qrexec agent for 60 seconds, see /var/log/xen/console/guest-ix2.log for details
    app_icon=dialog-warning
    expire-timeout=-1
    --
    [2025-11-09T07:58:36.926065-05]
    app_name=org.qubes.qui.tray.Domains
    summary=Qube Status: ix-test-for-blog-again
    body=Qube ix-test-for-blog-again has failed to start: Cannot connect to qrexec agent for 60 seconds, see /var/log/xen/console/guest-ix-test-for-blog-again.log for details
    app_icon=dialog-warning
    expire-timeout=-1
    

    To solve this, you can setup bind-dirs for Guix and then for Nix (no order here). Also make sure to not bloat the AppVM and TemplateVM with packages.

    If your qube get killed by dom0 in this process, the AppVM will have the directory setupped by bind-dirs corrupted, and to fix the it without creating a new AppVM, you can delete the (incomplete) copied directories /rw/bind-dirs/{gnu,nix,var}/ and decrease the copy period at startup by setupping them separately, then reboot AppVM to make it copy again.

    This is a annoying limitation that blocks from adding a prelude set of packages in TemplateVM. I still don't know how increase this timeout at dom0.

Outro

Thanks for reading, and happy reproducibilities.

Footnotes:

1

Ref: See sys_create_store at Guix install.sh

2

Note that default is /nix/var/nix/profiles/per-user/root/profile.

See the source code here.
Generated at 2025-11-11 Tue 11:17 by Emacs 29.4 (Org mode 9.6.15)