Getting Started with Guix for Reproducible Development Environments
I’ll be honest - when I first heard about GNU Guix, it sounded intimidating. Another package manager? With a functional programming twist? And what’s all this about “time machines” and “channels”?
But as it turns out, Guix solves a problem I’ve been wrestling with for years: how do you ensure that your development environment works exactly the same way across different machines and points in time? You know the drill - you set up a project on your laptop, it works perfectly, then six months later you try to run it on your server and nothing works because package versions have changed, dependencies have shifted, and you’re stuck playing detective to figure out what broke.
I spent some time recently setting up Guix for a personal blog project (this one, actually), and I was surprised by how straightforward it became once I understood the core concepts. Think about it like this: if Docker gives you reproducible containers, Guix gives you reproducible environments - but without the overhead and with much more granular control.
In this post, I’ll walk you through everything I learned about using Guix for per-project reproducible environments. We’ll cover the key concepts (channels, manifests, and time-machine), show you how to integrate it with direnv for automatic environment activation, and go through real examples that you can adapt for your own projects.
Why Guix for Development Environments?
Before we dive into the how, let’s talk about the why. If you’re like me, you’ve probably tried various approaches to managing development dependencies:
- Virtual environments (Python’s venv, Ruby’s rbenv): Great for language-specific dependencies, but what about system packages?
- Docker: Powerful but heavy, and you’re still managing Dockerfiles and base images
- Package managers (apt, brew, pacman): Work fine until you need different versions of the same package for different projects
- Language-specific tools (npm, pip, cargo): Each ecosystem has its own approach, leading to a patchwork of solutions
Guix takes a different approach. Instead of managing containers or language-specific environments, it manages packages at the system level with a functional approach. This means:
- True reproducibility: You can pin exact package versions and recreate the same environment years later
- No conflicts: Different projects can use different versions of the same package without interference
- Cross-platform consistency: The same environment works on your laptop, server, and colleague’s machine
- Rollback capability: Made a mistake? Roll back to a previous environment state instantly
Now, I won’t lie to you - there’s a learning curve. But once you get the hang of it, it’s incredibly powerful. Let me show you how it works.
Understanding Guix Channels
The first concept to wrap your head around is channels. Think of channels as package repositories, but with a twist - they’re Git repositories that contain package definitions, and you can pin them to specific commits for reproducibility.
Guix comes with a default channel (the main GNU Guix repository), but you’ll likely want to add others. The most common addition is the “nonguix” channel, which provides packages that can’t be included in the main Guix repository due to licensing restrictions (like proprietary software or packages with non-free dependencies).
Here’s what a basic channels.scm
file looks like:
;; Guix channels pinned for reproducibility
(list (channel
(name 'nonguix)
(url "https://gitlab.com/nonguix/nonguix")
(branch "master")
(commit "a96e2451bda5aaf9b48339edee392c6a3017d730")
(introduction
(make-channel-introduction
"897c1a470da759236cc11798f4e0a5f7d4d59fbc"
(openpgp-fingerprint
"2A39 3FFF 68F4 EF7A 3D29 12AF 6F51 20A0 22FB B2D5"))))
(channel
(name 'guix)
(url "https://git.savannah.gnu.org/git/guix.git")
(branch "master")
(commit "80651b889926a304a671092ad2fc223440845a70")
(introduction
(make-channel-introduction
"9edb3f66fd807b096b48283debdcddccfea34bad"
(openpgp-fingerprint
"BBB0 2DDF 2CEA F6A8 0D1D E643 A2A0 6DF2 A33A 54FA")))))
This might look intimidating at first, but it’s actually quite straightforward. Each channel entry specifies:
- name: The channel identifier
- url: Where to find the channel’s Git repository
- commit: The exact Git commit to use (this is the key to reproducibility!)
- introduction: Cryptographic verification information (for security)
The beauty of this approach is that you’re not just saying “use the latest version of nonguix” - you’re saying “use exactly this commit of nonguix.” This means that months or years from now, you can recreate the exact same package set.
Getting Your Current Channel State
You don’t have to write these files by hand. Guix provides a handy command to snapshot your current channel configuration:
guix describe --format=channels > channels.scm
This captures whatever channels you currently have configured along with their exact commit hashes. It’s like taking a snapshot of your package universe at a specific point in time.
Package Manifests: Your Project’s Bill of Materials
Now that you understand channels, let’s talk about manifests. If channels define where packages come from, manifests define which packages you actually want. Think of a manifest as your project’s bill of materials - a list of everything needed to recreate your development environment.
Here’s a simple manifest file:
;; Simple manifest for a Python data science project
(specifications->manifest
'("python-wrapper" ; Provides 'python' command (not just 'python3')
"python-numpy"
"python-pandas"
"python-matplotlib"
"jupyter"
"python-ipython"
"python-notebook"
"python-nbconvert"
"git"
"make"))
As it turns out, this approach is much more explicit than language-specific dependency files. Instead of relying on a requirements.txt
that might resolve differently depending on when you install it, you’re specifying exactly which packages you need from the Guix ecosystem.
A Real Example: Blog Development Environment
For my blog setup, I needed Hugo (a specific old version that works with my theme), Python for data analysis, and Jupyter for notebooks. Here’s what my manifest looks like:
;; Blog development environment
(specifications->manifest
'("python-wrapper"
"python-numpy"
"python-pandas"
"python-matplotlib"
"jupyter"
"python-ipython"
"python-ipykernel"
"python-notebook"
"python-nbconvert"
"python-nbformat"
"python-attrs"
"python-bleach"
"python-jinja2"
"python-jsonschema"
"python-markupsafe"
"python-pygments"
"python-pytz"
"python-six"
"python-tornado"))
You might notice that Hugo isn’t in this list. That’s because the version of Hugo available in the current Guix channels (v0.140.2) breaks my blog theme, which only works with versions before 0.120.0. This brings us to our next topic: getting specific package versions.
Getting Specific Package Versions with Time-Machine
Here’s where Guix really shines. Most package managers give you limited options for getting older versions of packages. You might find a few old versions in the repository, but good luck getting that specific version from six months ago that you know works with your project.
Guix’s time-machine feature lets you travel back to any point in Guix’s history and install packages from that era. It’s like having access to every version of every package that was ever available.
The Time-Machine Approach
The basic idea is simple: instead of using packages from the current Guix channels, you use packages from Guix channels as they existed at a specific point in time. Here’s how you’d use it:
# Use packages from a specific commit
guix time-machine --channels=channels.scm -- environment --manifest=manifest.scm
This command does several things:
- Checks out the channels at the commits specified in
channels.scm
- Creates an environment with the packages listed in
manifest.scm
- Uses the package versions that were available at that historical point
When Time-Machine Gets Tricky
Now, I’ll be honest - time-machine isn’t always smooth sailing. During my blog setup, I ran into issues with old channel commits that referenced packages or dependencies that no longer existed. It’s particularly tricky with the nonguix channel, which sometimes has compatibility issues with older Guix commits.
But here’s where Guix’s flexibility really shines: you can define custom packages directly in your manifest. Instead of relying on historical channels or manual downloads, I looked at how Hugo is packaged in the nonguix repository (https://gitlab.com/nonguix/nonguix/-/blob/master/nongnu/packages/hugo.scm) and adapted it for my needs.
The key insight is that Hugo is a single Go binary with no complex dependencies which is perfect for a custom package definition. Here’s what my updated manifest looks like:
;; Blog development environment with custom Hugo v0.100.0
(use-modules (gnu)
(guix packages)
(guix profiles)
(guix download)
(guix licenses)
(nonguix build-system binary))
;; Define Hugo v0.100.0 locally
(define hugo-0.100.0
(package
(name "hugo")
(version "0.100.0")
(source
(origin
(method url-fetch)
(uri (string-append
"https://github.com/gohugoio/hugo/releases/download/v" version
"/hugo_extended_" version "_Linux-64bit.tar.gz"))
(sha256
(base32 "1rijpir80vx2hxkmgmzg2ac784bbh78vf0gcfyqm4la6qca48qln"))))
(build-system binary-build-system)
(arguments
`(#:install-plan '(("hugo" "/bin/hugo"))
#:validate-runpath? #f)) ; Skip validation for binary package
(home-page "https://gohugo.io/")
(synopsis "Static site generator written in Go (version 0.100.0)")
(description "Hugo v0.100.0 for compatibility with older themes.")
(license asl2.0)))
;; Create manifest with both Hugo and Python packages
(packages->manifest
(append
(list hugo-0.100.0)
(map specification->package
'("python-wrapper"
"python-numpy"
"python-pandas"
; ... other packages
))))
This approach gives you the best of both worlds: proper Guix package management with exact version control. Since Hugo releases are just single binaries with predictable URLs, it’s straightforward to create package definitions for any version you need.
Integrating with Direnv for Automatic Environment Activation
All of this is great, but you don’t want to manually activate your environment every time you enter your project directory. This is where direnv comes in - it’s a tool that automatically loads and unloads environment variables based on your current directory.
Direnv has built-in support for Guix, making it incredibly easy to set up per-project environments that activate automatically when you cd
into your project directory.
Basic Direnv + Guix Setup
The simplest setup uses just a manifest file. Create a .envrc
file in your project root:
use guix --manifest=manifest.scm
When you run direnv allow
, direnv will automatically run guix shell --manifest=manifest.scm
whenever you enter the directory, and clean up the environment when you leave.
Advanced Setup with Time-Machine
For more complex setups that use time-machine, you can call it directly in your .envrc
:
# Use time-machine with pinned channels for reproducibility
eval "$(guix time-machine --channels=channels.scm -- environment --manifest=manifest.scm --search-paths)"
That’s it! Since our Hugo package is defined directly in the manifest, everything gets handled automatically by Guix. No manual scripts, no PATH manipulation - just pure, reproducible package management.
This setup does everything automatically when you enter the directory:
- Creates a Guix environment using historical package versions for Python packages
- Builds and installs our custom Hugo v0.100.0 package
- Makes both Hugo and all Python tools available in your PATH
The Development Experience
Once you have this set up, the development experience is seamless. Here’s what it looks like in practice:
# Outside the project directory
$ which python
/usr/bin/python3
# Enter the project directory
$ cd my-blog-project
direnv: loading ~/my-blog-project/.envrc
Computing Guix derivation for 'x86_64-linux'...
# Now you have the project environment
$ which python
/gnu/store/...-python-wrapper-3.10.7/bin/python
$ python --version
Python 3.10.7
$ hugo version
hugo v0.100.0-27b077544d8efeb85867cb4cfb941747d104f765 linux/amd64
# Leave the project directory
$ cd ..
direnv: unloading
# Back to system packages
$ which python
/usr/bin/python3
It’s incredibly smooth once you get it working. No more “wait, which Python version am I using?” or “did I remember to activate my virtual environment?”
Common Workflows and Patterns
Now that we’ve covered the core concepts, let me share some patterns I’ve found useful for different types of projects.
The Simple Approach: Current Packages
For many projects, you don’t need historical packages - you just want isolation from your system packages and the ability to reproduce the environment later. This is the simplest approach:
;; manifest.scm
(specifications->manifest
'("python-wrapper"
"nodejs"
"git"
"make"))
# .envrc
use guix --manifest=manifest.scm
This gives you a clean, isolated environment with the packages you need, and you can commit both files to version control for your teammates.
The Pinned Approach: Maximum Reproducibility
When you need to ensure that your environment stays exactly the same over time, use the pinned channel approach:
- Set up your environment with the packages you need
- Snapshot your channels:
guix describe --format=channels > channels.scm
- Create your manifest with the packages you’re using
- Set up direnv to use time-machine:
eval "$(guix time-machine --channels=channels.scm -- environment --manifest=manifest.scm --search-paths)"
The Hybrid Approach: Guix + Manual Packages
Sometimes you need packages that aren’t available in Guix, or you need specific versions that are problematic with time-machine. In these cases, you can combine Guix-managed packages with manually installed ones:
# .envrc
use guix --manifest=manifest.scm
# Install specific tools locally
./install-custom-tools.sh
# Add local tools to PATH
PATH_add .local/bin
This approach lets you get the benefits of Guix for most packages while handling edge cases manually.
Language-Specific Patterns
For different programming languages, you’ll want to adjust your approach:
Python Projects:
- Always use
python-wrapper
to get thepython
command (not justpython3
) - Include
python-pip
if you need to install additional packages with pip - Consider including
python-virtualenv
if you want to use virtual environments on top of Guix
Node.js Projects:
- Include
nodejs
andnpm
in your manifest - You might want
node-lts
for long-term support versions - Consider including common tools like
node-typescript
if you use TypeScript
Go Projects:
- Include
go
in your manifest - Go’s module system works well with Guix environments
- You might want
go-tools
for additional development tools
Troubleshooting Common Issues
As you start using Guix for development environments, you’ll likely run into some common issues. Here are the ones I encountered and how to solve them:
“Package not found” Errors
If Guix can’t find a package you’re looking for:
- Search for it:
guix search package-name
- Check different names: Python packages often have
python-
prefixes - Add the nonguix channel if it’s a proprietary or non-free package
- Check if it’s available in a different channel
Time-Machine Failures
When time-machine fails to build historical environments:
- Try a different historical commit - some commits have broken dependencies
- Use a more recent commit - older commits are more likely to have issues
- Skip time-machine for problematic packages and install them manually
- Check the build logs to understand what’s failing
Direnv Not Working
If direnv isn’t activating your environment:
- Make sure you ran
direnv allow
in the project directory - Check that direnv is installed and properly configured in your shell
- Look at the direnv logs:
direnv reload
will show you what’s happening - Test your
.envrc
manually by running the commands directly
Slow Environment Loading
If your environment takes a long time to load:
- Guix builds packages the first time - subsequent loads will be faster
- Use substitutes to download pre-built packages instead of building from source
- Consider using profiles for frequently-used package combinations
- Cache time-machine environments by using them regularly
Best Practices and Tips
After working with Guix environments for a while, here are some practices I’ve found helpful:
Keep Your Manifests Simple
Start with just the packages you actually need. You can always add more later, but it’s easier to debug a simple manifest than a complex one.
Version Control Everything
Always commit your manifest.scm
, channels.scm
, and .envrc
files to version control. This ensures that anyone who clones your project can recreate your exact environment.
Document Your Setup
Include a README section that explains how to get started with your Guix environment:
## Development Environment
This project uses GNU Guix for reproducible development environments.
1. Install GNU Guix and direnv
2. Clone this repository
3. Run `direnv allow` in the project directory
4. The environment will automatically activate when you enter the directory
Test on Multiple Machines
If possible, test your Guix environment on different machines to make sure it’s truly reproducible. This is especially important if you’re collaborating with others.
Keep Backups of Working Configurations
When you have a working environment, consider creating a backup of your channels.scm
file. If you update your channels and something breaks, you can easily roll back to a known-good state.
Use Profiles for Common Setups
If you frequently use the same set of packages across multiple projects, consider creating a Guix profile for them:
guix install --profile=~/.guix-profiles/data-science python-pandas python-numpy jupyter
Then you can activate this profile when needed without recreating the environment each time.
Comparing Guix to Other Solutions
It’s worth putting Guix in context with other approaches you might be familiar with:
Guix vs. Docker
Docker pros:
- Widely adopted and well-understood
- Great for deployment as well as development
- Extensive ecosystem of pre-built images
Guix pros:
- Much lighter weight (no container overhead)
- More granular control over packages
- Better integration with host system
- True reproducibility at the package level
Guix vs. Nix
Guix and Nix are actually quite similar - both use functional package management principles. The main differences:
Nix:
- Larger ecosystem and community
- More mature tooling (like nix-shell, flakes)
- Uses its own DSL for package definitions
Guix:
- Uses Scheme (Lisp) for package definitions
- More principled about free software
- Better integration with GNU/Linux systems
Guix vs. Language-Specific Tools
Language-specific tools (venv, rbenv, etc.):
- Simpler to understand and use
- Better integration with language ecosystems
- Limited to single languages
Guix:
- Works across all languages and system packages
- More complex but more powerful
- True system-level reproducibility
Conclusion
I’ll admit, when I started this journey, I wasn’t sure if Guix was worth the learning curve. But now that I have it set up, I’m really impressed with what it provides. The ability to pin exact package versions and recreate environments months or years later is incredibly valuable, especially for long-term projects or research that needs to be reproducible.
The integration with direnv makes the day-to-day experience seamless - I don’t have to think about activating environments or managing dependencies manually. When I enter a project directory, everything just works, with exactly the package versions that project needs.
Is Guix right for every project? Probably not. If you’re working on a simple web app that uses standard, current versions of everything, the traditional approaches (Docker, language-specific tools) might be simpler. But if you’re working on projects where reproducibility matters - research, long-term systems, or anything that needs to work consistently across time and machines - Guix is worth considering.
The key things to remember:
- Channels define where packages come from - pin them for reproducibility
- Manifests define which packages you want - keep them in version control
- Time-machine lets you use historical package versions - incredibly powerful but can be tricky
- Direnv integration makes everything automatic - set it once, use it everywhere
- Start simple and gradually add complexity - you don’t need to use every feature at once
All in all, I’m glad I took the time to learn Guix. It’s not the easiest tool to get started with, but once you understand the concepts, it provides a level of control and reproducibility that’s hard to match with other approaches. And now that I have a working setup, creating new reproducible environments is actually quite straightforward.
If you’re dealing with “works on my machine” problems or need to ensure your development environment stays consistent over time, give Guix a try. Just be prepared for a bit of a learning curve - but I think you’ll find it’s worth it.