<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Guix on Wai Hon's Blog</title><link>https://whhone.com/tags/guix/</link><description>Recent content in Guix on Wai Hon's Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>whhone@gmail.com (Wai Hon Law)</managingEditor><webMaster>whhone@gmail.com (Wai Hon Law)</webMaster><lastBuildDate>Sun, 13 Sep 2026 09:00:00 -0700</lastBuildDate><atom:link href="https://whhone.com/tags/guix/index.xml" rel="self" type="application/rss+xml"/><item><title>Switching to GNU Guix: A Beginner's Perspective</title><link>https://whhone.com/posts/switching-to-gnu-guix/</link><pubDate>Sun, 13 Sep 2026 09:00:00 -0700</pubDate><author>whhone@gmail.com (Wai Hon Law)</author><guid>https://whhone.com/posts/switching-to-gnu-guix/</guid><description>&lt;h2 id="background-a-decade-of-arch-linux"&gt;Background: A Decade of Arch Linux&lt;/h2&gt;
&lt;p&gt;Arch Linux was my distribution of choice for more than a decade. With its rolling-release model, minimal base, and the invaluable ArchWiki, it felt like the final distribution I would ever need.&lt;/p&gt;
&lt;p&gt;My primary Linux machine is a dedicated home server, handling services like Home Assistant, local DNS, background jobs, and developer sandboxes. For a server running 24/7, long-term stability and maintainability are critical. Over years of incremental tweaks, configuration entropy inevitably crept in. System state became scattered across &lt;code&gt;/etc&lt;/code&gt;, &lt;code&gt;/usr&lt;/code&gt;, systemd service units, and package manager transactions. Whenever I made changes, I had to keep diligent notes about which files were edited, when, and why.&lt;/p&gt;
&lt;p&gt;Recent events, such as the Arch Linux AUR security incidents (which I touched upon in my &lt;a href="https://whhone.com/posts/using-caddy-with-extensions-on-archlinux/"&gt;previous post on Caddy&lt;/a&gt;) and developments around Omarchy, prompted me to re-evaluate my setup. I wanted an operating system that was declarative, reproducible, and manageable entirely in code.&lt;/p&gt;
&lt;h2 id="nixos-vs-gnu-guix"&gt;NixOS vs GNU Guix&lt;/h2&gt;
&lt;p&gt;Declarative operating systems offer a compelling answer to configuration drift. When researching options, NixOS was actually my first choice.&lt;/p&gt;
&lt;h3 id="testing-nixos-in-a-vm"&gt;Testing NixOS in a VM&lt;/h3&gt;
&lt;p&gt;I spun up a NixOS virtual machine and spent time experimenting by replicating the core services I was running on Arch to ensure everything worked properly. It worked really well: declaring the entire system state in a configuration file with instant rollback capabilities felt like the right model for operating systems.&lt;/p&gt;
&lt;p&gt;However, as I explored deeper, documentation in NixOS became a major source of friction. The newer &lt;code&gt;nix&lt;/code&gt; command line interface and Flakes remain experimental features that are not yet enabled by default or standardized across the ecosystem, leading to divergent documentation and tutorials. Finding guidance was further complicated by the presence of two separate wikis.&lt;/p&gt;
&lt;h3 id="discovering-gnu-guix"&gt;Discovering GNU Guix&lt;/h3&gt;
&lt;p&gt;While learning more about NixOS, I came across David Wilson&amp;rsquo;s video from System Crafters: &lt;a href="https://www.youtube.com/watch?v=PiAMRXYIri0"&gt;Why I Choose Guix Over NixOS&lt;/a&gt;. As a fan of David Wilson, his arguments resonated strongly with me. Shortly after, I also watched YouTux&amp;rsquo;s video, &lt;a href="https://www.youtube.com/watch?v=MtYWp8sCsiE&amp;amp;t"&gt;One of the Best Linux Distros Isn&amp;rsquo;t Even in DistroWatch&amp;rsquo;s Top 100&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;These videos prompted me to research GNU Guix and try it inside a VM.&lt;/p&gt;
&lt;p&gt;GNU Guix shares the same core architectural foundation as NixOS (functional package management, declarative configuration, and atomic rollbacks), but its design choices felt much more cohesive:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Language (GNU Guile Scheme vs Nix DSL):&lt;/strong&gt; Nix uses its own bespoke domain-specific language. Guix configurations are written entirely in GNU Guile, a general-purpose Scheme (Lisp). As an Emacs user accustomed to Emacs Lisp, Scheme felt familiar and expressive. Rather than learning a specialized configuration syntax, I could leverage a real programming language with first-class functions, macros, and modules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Init System (GNU Shepherd vs systemd):&lt;/strong&gt; NixOS builds on systemd, while Guix System uses GNU Shepherd as its service manager. In Guix, Shepherd services are also defined in Guile Scheme. Everything from package recipes to system daemons to PID 1 shares a unified language and data model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; Guix&amp;rsquo;s documentation is remarkably cohesive. Even though some community tutorials can be dated, the official GNU Guix reference manual is consistent, comprehensive, and avoids the fragmented wiki landscape of Nix.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Philosophy (GNU Libre Standards vs Pragmatism):&lt;/strong&gt; NixOS takes a pragmatic stance, offering toggles for proprietary software and unfree drivers. GNU Guix strictly adheres to the GNU Free System Distribution Guidelines, shipping the Linux-libre kernel and free software exclusively by default.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This philosophical strictness has its trade-offs. For my home server, which connects to the network through an Ethernet cable, proprietary Wi-Fi firmware is unnecessary, and the Linux-libre kernel works out of the box. The purity and auditability feel satisfying, though it does mean dealing with a more curated package catalog.&lt;/p&gt;
&lt;p&gt;Between the familiarity of Scheme, the unified architecture, and its close kinship with Emacs, I decided to make the switch to GNU Guix.&lt;/p&gt;
&lt;h2 id="what-i-like-as-a-beginner"&gt;What I Like as a Beginner&lt;/h2&gt;
&lt;h3 id="unified-declarative-config-in-git"&gt;Unified Declarative Config in Git&lt;/h3&gt;
&lt;p&gt;With GNU Guix, the entire operating system configuration lives in code and is tracked in Git. I manage both Guix System (operating system declarations, system daemons, kernel parameters) and Guix Home (user packages, shell environments, and dotfiles) within a single literate Org-mode file (&lt;code&gt;guix.org&lt;/code&gt;) using Org Babel.&lt;/p&gt;
&lt;p&gt;At any point, I can see exactly which packages are installed and which services are active directly from the codebase. In the past, I hesitated to invest in complex system configurations because maintaining them across updates was fragile. With Guix, configuring the operating system feels as manageable and predictable as tweaking my Emacs configuration.&lt;/p&gt;
&lt;h3 id="flexible-guile-configuration"&gt;Flexible Guile Configuration&lt;/h3&gt;
&lt;p&gt;Guix configurations are written in a full-featured programming language rather than static YAML or JSON. This gives immense flexibility when composing services.&lt;/p&gt;
&lt;p&gt;Using Guix&amp;rsquo;s service extension mechanism with &lt;code&gt;simple-service&lt;/code&gt;, you can extend existing system services cleanly without modifying base declarations. In a traditional distribution, deploying a service forces you to fragment its configuration across completely separate subsystems: a systemd unit in &lt;code&gt;/etc/systemd/system/&lt;/code&gt;, directory initialization in &lt;code&gt;/etc/tmpfiles.d/&lt;/code&gt;, and reverse proxy blocks in &lt;code&gt;/etc/nginx/conf.d/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;With Guix, you can co-locate a service and its surrounding infrastructure side by side in the exact same configuration block:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-scheme" data-lang="scheme"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;;; Home Assistant container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;simple-service&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;home-assistant-container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;oci-service-type&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;oci-extension&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;containers&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;oci-container-configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;provision&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;home-assistant&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;image&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;ghcr.io/home-assistant/home-assistant:stable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;details&lt;/span&gt; &lt;span class="nv"&gt;config&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;;; Inject the Nginx reverse proxy configuration for Home Assistant&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;simple-service&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;home-assistant-nginx-server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;nginx-service-type&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;nginx-server-configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inherit&lt;/span&gt; &lt;span class="nv"&gt;ssl-server-configuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;server-name&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;string-append &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;home.&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;locations&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;nginx-location-configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;details&lt;/span&gt; &lt;span class="nv"&gt;config&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This architectural clarity makes understanding, modifying, or removing a service self-contained and painless.&lt;/p&gt;
&lt;h3 id="streamlined-shepherd-timers"&gt;Streamlined Shepherd Timers&lt;/h3&gt;
&lt;p&gt;Defining scheduled jobs in Guix is also much more streamlined than in traditional distributions. In systemd, setting up a recurring job requires declaring a &lt;code&gt;.service&lt;/code&gt; file and a separate &lt;code&gt;.timer&lt;/code&gt; unit.&lt;/p&gt;
&lt;p&gt;With GNU Shepherd in Guix, you can declare the executable script, its dependencies, and its schedule together in a single Guile expression:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-scheme" data-lang="scheme"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let* &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nf"&gt;duckdns-script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;program-file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;duckdns-update&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;with-extensions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list &lt;/span&gt;&lt;span class="nv"&gt;guile-gnutls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;;required by (web client)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="nv"&gt;~&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;use-modules&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ice-9&lt;/span&gt; &lt;span class="nv"&gt;textual-ports&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;web&lt;/span&gt; &lt;span class="nv"&gt;client&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nf"&gt;token&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;string-trim-both&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;call-with-input-file &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/etc/secrets/duckdns.token&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;get-string-all&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;query-template&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;string-append &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;https://www.duckdns.org/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;update?domains=&amp;lt;mydomain&amp;gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;&amp;amp;token=~a&amp;amp;ip=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;http-get&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt; &lt;span class="no"&gt;#f&lt;/span&gt; &lt;span class="nv"&gt;query-template&lt;/span&gt; &lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)))))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;duckdns-timer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;shepherd-timer&lt;/span&gt; &lt;span class="o"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;duckdns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;*/5 * * * *&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="nv"&gt;~&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="nv"&gt;$duckdns-script&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="nv"&gt;:requirement&lt;/span&gt; &lt;span class="o"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;networking&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="nv"&gt;:documentation&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;Update personal domain IP on DuckDNS every 5 minutes.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;simple-service&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;duckdns-timer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;shepherd-root-service-type&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list &lt;/span&gt;&lt;span class="nv"&gt;duckdns-timer&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="sandboxed-containers-with-guix-shell"&gt;Sandboxed Containers with &lt;code&gt;guix shell&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;guix shell&lt;/code&gt; command has transformed how I run ad-hoc software. Instead of polluting my profile with one-off utilities, &lt;code&gt;guix shell&lt;/code&gt; creates an ephemeral environment that is cleaned up when the session ends.&lt;/p&gt;
&lt;p&gt;Furthermore, its container mode (&lt;code&gt;guix shell -C&lt;/code&gt; or &lt;code&gt;--container&lt;/code&gt;) makes lightweight isolation trivial. By specifying exactly which directories (&lt;code&gt;--share&lt;/code&gt;) and network access (&lt;code&gt;--network&lt;/code&gt;) to expose, I can run untrusted commands or AI coding agents inside an isolated sandbox.&lt;/p&gt;
&lt;p&gt;For example, I run the Antigravity CLI within a sandbox granting access only to the current working directory, its configuration, and necessary binaries:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run Antigravity CLI in a Guix sandbox with access to its config and the project directories.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agy-guix&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; guix shell --container --network --emulate-fhs &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --share&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --share&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.gemini&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --share&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.local/bin&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --preserve&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;^(TERM)$&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; coreutils nss-certs bash guix guile emacs git ripgrep fd zip unzip -- &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.local/bin/agy &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Knowing an agent cannot access arbitrary files outside its granted path makes experimentation much safer.&lt;/p&gt;
&lt;h3 id="purity-and-the-minimal-bootstrap-seed"&gt;Purity and the Minimal Bootstrap Seed&lt;/h3&gt;
&lt;p&gt;By default, GNU Guix is strictly libre. It ships with the Linux-libre kernel and avoids proprietary binary blobs.&lt;/p&gt;
&lt;p&gt;Beyond day-to-day use, Guix&amp;rsquo;s architectural focus on bootstrapping integrity (reducing the bootstrap binary seed down to around 357 bytes through the stage0/Mes bootstrap) provides a strong sense of technical rigor. Knowing the system can be built from minimal, auditable foundations provides real trust in the underlying stack.&lt;/p&gt;
&lt;h2 id="issues-and-how-i-handle-them"&gt;Issues and How I Handle Them&lt;/h2&gt;
&lt;h3 id="slow-guix-pull-and-source-builds"&gt;Slow &lt;code&gt;guix pull&lt;/code&gt; and Source Builds&lt;/h3&gt;
&lt;p&gt;A fundamental difference between Guix and Arch Linux is that Guix is a source-based distribution at its core, backed by substitute servers that distribute prebuilt binaries.&lt;/p&gt;
&lt;p&gt;If a newly pulled channel commit has not yet been built by the substitute build farm (such as &lt;code&gt;ci.guix.gnu.org&lt;/code&gt; or Bordeaux), your machine will fall back to compiling the packages locally. While this architecture empowers powerful capabilities like &lt;code&gt;guix challenge&lt;/code&gt; (verifying build reproducibility against other servers) and &lt;code&gt;guix time-machine&lt;/code&gt; (traveling back to any historical revision), waiting for long local builds on a home server can be tedious.&lt;/p&gt;
&lt;p&gt;To avoid unexpected local compilation, my practical workaround is to pin &lt;code&gt;guix pull&lt;/code&gt; to a specific commit from a day or two earlier, ensuring substitutes are already built and cached:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-scheme" data-lang="scheme"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;;; ~/.config/guix/channels.scm (or: guix pull --commit=&amp;lt;hash&amp;gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inherit&lt;/span&gt; &lt;span class="nv"&gt;%default-guix-channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;;; faster pull via Codeberg mirror&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;https://codeberg.org/guix/guix.git&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;5b52edf051a020947b1d4859853799f2bbce176e&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="addressing-the-package-gap"&gt;Addressing the Package Gap&lt;/h3&gt;
&lt;p&gt;The most noticeable hurdle for a beginner coming from Arch Linux is repository size. The official Guix channel has strict libre standards and a smaller catalog than the Arch User Repository (AUR). Common utilities like &lt;code&gt;hugo&lt;/code&gt; and &lt;code&gt;caddy&lt;/code&gt; are not present in the official channel.&lt;/p&gt;
&lt;p&gt;In practice, there are several practical ways I handle this gap:&lt;/p&gt;
&lt;h4 id="1-dot-writing-custom-package-definitions"&gt;1. Writing Custom Package Definitions&lt;/h4&gt;
&lt;p&gt;Writing a package definition in Scheme is straightforward. You can define a package that builds from source or downloads an official upstream release archive:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-scheme" data-lang="scheme"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;define &lt;/span&gt;&lt;span class="nv"&gt;hugo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;package&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;hugo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;version&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;0.165.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;source&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;origin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;method&lt;/span&gt; &lt;span class="nv"&gt;url-fetch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;uri&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;string-append&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;https://github.com/gohugoio/hugo/releases/download/v&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;version&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;/hugo_&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;version&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;_linux-amd64.tar.gz&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;base32&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;0...&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;build-system&lt;/span&gt; &lt;span class="nv"&gt;trivial-build-system&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For Hugo, I created a local package definition downloading the official prebuilt binary, making it available seamlessly to my system and deploy scripts.&lt;/p&gt;
&lt;h4 id="2-dot-running-ephemeral-toolchains-via-guix-shell"&gt;2. Running Ephemeral Toolchains via &lt;code&gt;guix shell&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;For tools that exist within language ecosystems, &lt;code&gt;guix shell&lt;/code&gt; can pair with ecosystem runners (&lt;code&gt;uvx&lt;/code&gt;, &lt;code&gt;npx&lt;/code&gt;) without installing packages globally:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# That is how I publish my blog to Cloudflare now.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;guix shell node -- npx -y wrangler
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="3-dot-generating-definitions-with-guix-import"&gt;3. Generating Definitions with &lt;code&gt;guix import&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;When a package is missing, &lt;a href="https://guix.gnu.org/manual/1.5.0/en/html_node/Invoking-guix-import.html"&gt;&lt;code&gt;guix import&lt;/code&gt;&lt;/a&gt; can automatically generate package definitions from upstream registries, such as PyPI, Crates.io, CPAN, or GNU ELPA. This significantly reduces the manual effort of writing package recipes.&lt;/p&gt;
&lt;h4 id="4-dot-switching-to-readily-available-alternatives"&gt;4. Switching to Readily Available Alternatives&lt;/h4&gt;
&lt;p&gt;Sometimes the simplest path is adopting software that is already a first-class citizen in Guix. Rather than maintaining a custom Caddy setup with third-party plugins, I switched back to Nginx combined with Certbot. Both are well-supported native services in Guix System, simplifying long-term maintenance.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;It has been a month since migrating my home server to GNU Guix. Managing OS state declaratively through Git has eliminated configuration drift, and Guile Scheme provides a cohesive environment that complements Emacs. While adapting to a smaller package ecosystem and managing substitute timing requires occasional adjustments, the stability, reproducibility, and container isolation make it a dependable foundation.&lt;/p&gt;
&lt;p&gt;In my free time, I have started reading the legendary SICP (Structure and Interpretation of Computer Programs) to deepen my understanding of Scheme and functional programming.&lt;/p&gt;</description></item></channel></rss>