Deploy and Manage a Guix VM in OpenStack: A Comprehensive Guide
Introduction
It's been a few months since I started using Guix as the operating system on my personal laptop, and I've been continuously impressed by its unique approach to system management. Unlike traditional package managers, Guix offers a fully declarative, functional approach to system configuration. This means that your entire system—from packages to services to system parameters—can be defined in code and reproducibly deployed.
What makes Guix particularly powerful is its use of Guile Scheme as the configuration language, offering the full expressiveness of a proper programming language rather than a limited DSL. The ability to treat your system configuration as code, with all the benefits of version control, reproducibility, and atomicity, sets Guix apart from other Linux distributions.
After becoming more comfortable with Guile as a programming language, I decided to extend my Guix experience to the cloud by deploying a virtual machine in OpenStack (through OVHcloud's public cloud offering) and managing it with guix deploy. This post documents my experience and provides a step-by-step guide to help others do the same.
Prerequisites
Before we begin, you'll need:
- A working Guix System installation (for building the image)
- Basic familiarity with Guile Scheme
- An OpenStack account (I'm using OVHcloud)
- OpenStack CLI tools installed
- SSH key pair for secure access
If you're new to Guix, I recommend first spending some time with the official manual to understand the basic concepts.
Building the QCow2 Image Locally
Creating a QCow2 image with Guix is surprisingly straightforward—comparable to writing a Dockerfile for creating a container image. The process begins with defining an operating-system configuration.
Here's the configuration I used:
;; image.scm
(use-modules (gnu)
(nongnu packages linux)
(guix packages)
(guix build-system copy))
(use-service-modules networking ssh web)
(use-package-modules bootloaders ssh web version-control)
(operating-system
(kernel linux)
(firmware (list linux-firmware))
(host-name "guix-1")
(locale "en_US.utf8")
(timezone "Europe/Paris")
(keyboard-layout (keyboard-layout "fr"))
(bootloader (bootloader-configuration
(bootloader grub-bootloader)
(target "/dev/vda")
(terminal-outputs '(console))))
(file-systems (cons (file-system
(mount-point "/")
(device "/dev/vda1")
(type "ext4"))
%base-file-systems))
(services
(append
(list
(service dhcp-client-service-type)
(service openssh-service-type
(openssh-configuration
(openssh openssh-sans-x)
(permit-root-login #t)
(password-authentication? #f) ;; More secure: SSH keys only
(authorized-keys
;; Authorize our SSH key
`(("root" ,(local-file "/home/polyedre/.ssh/id_rsa.pub"))
("polyedre" ,(local-file "/home/polyedre/.ssh/id_rsa.pub")))))))
(modify-services %base-services
(guix-service-type config =>
(guix-configuration
(inherit config)
(authorized-keys (append (list (local-file "/etc/guix/signing-key.pub")) %default-authorized-guix-keys))))))))
This file declares an operating system that:
- Uses the Linux kernel and additional non-free firmware drivers
- Sets up basic system parameters (hostname, locale, timezone, keyboard layout)
- Configures GRUB for the virtual device
- Sets up the root filesystem on
/dev/vda1 - Enables two critical services:
- A DHCP client to get the IP automatically - An OpenSSH daemon that authorizes connections to the root user with the public key from my local laptop
With this configuration ready, building the image is as simple as:
guix system image --root=guix-result --image-type=qcow2 image.scm
The --root flag creates a symlink named guix-result that points to the generated artifact.
Since guix-result is a read-only artifact, you'll need to copy it to a writable location if you want to test it locally:
cp guix-result guix.qcow2
You can then start the virtual machine using QEMU:
guix shell qemu -- sudo qemu-system-x86_64 \
-nic user,model=virtio-net-pci \
-enable-kvm \
-m 2048 \
-device virtio-blk,drive=guix-demo \
-drive if=none,file=guix.qcow2,id=guix-demo
Uploading the Image and Starting a VM
To work with OpenStack, you'll first need to install the CLI tools:
pip install python-openstackclient
After configuring your OpenStack credentials (typically by sourcing an environment file provided by your cloud provider), you can upload the image:
openstack image create \
--progress \
--disk-format qcow2 \
--container-format bare \
--file guix-result \
--property hypervisor_type=kvm \
--property os_type=linux \
--property os_admin_user=root \
--property os_distro=guixsystem \
--property os_version=unknown \
guix
Now you can launch a VM from this image. The following command creates a VM named guix-1 with 8GB of memory, connected to the Ext-Net network (which provides public IPs in OVHcloud's OpenStack):
openstack server create \
--image guix \
--network Ext-Net \
--flavor b3-8 \
--key-name laptop \
guix-1
After a minute or two, you can check the status and IP address of your VM:
$ openstack server list
+--------------------------------------+--------+--------+--------------------------------------------------+-------+--------+
| ID | Name | Status | Networks | Image | Flavor |
+--------------------------------------+--------+--------+--------------------------------------------------+-------+--------+
| af308c4e-8064-44c3-8ced-ed7c112478eb | guix-1 | ACTIVE | Ext-Net=2001:41d0:304:300::627d, 217.182.210.193 | guix | b3-8 |
+--------------------------------------+--------+--------+--------------------------------------------------+-------+--------+
And connect to it:
$ ssh root@217.182.210.193
Last login: Fri May 16 17:50:33 2025 from 81.65.157.5
root@guix-1 ~#
Cloud-Init Integration
One limitation of our initial image is the lack of cloud-init, which means features like automatic disk resizing don't work. Let's address this by integrating cloud-init with our Guix system.
First, update the image.scm file to include cloud-init:
;; Updated image.scm with cloud-init
(use-modules (gnu)
(nongnu packages linux)
(gnu packages cloud)
(guix packages)
(guix build-system copy))
(use-service-modules networking ssh web cloud) ;; Added cloud
(use-package-modules bootloaders ssh web version-control)
(operating-system
;; Previous configuration...
(services
(append
(list
(service dhcp-client-service-type)
(service openssh-service-type
(openssh-configuration
(openssh openssh-sans-x)
(permit-root-login #t)
(password-authentication? #f)
(authorized-keys
`(("root" ,(local-file "/home/polyedre/.ssh/id_rsa.pub"))
("polyedre" ,(local-file "/home/polyedre/.ssh/id_rsa.pub"))))))
;; Add cloud-init service
(service cloud-init-service-type
(cloud-init-configuration
(datasources '("ConfigDrive" "OpenStack" "NoCloud")))))
;; Rest of configuration...
)))
With cloud-init integrated, your VM will now handle cloud-specific tasks automatically, including:
- Resizing the root partition to use all available space
- Setting the hostname based on cloud metadata
- Configuring networking based on cloud metadata
- Handling SSH keys from the cloud provider
Build the updated image and upload it to OpenStack following the same steps as before.
Managing the VM with guix deploy
One of Guix's most powerful features is guix deploy, which allows you to manage remote systems using the same declarative approach used for local system configuration. This provides a consistent experience across all your machines and simplifies ongoing maintenance.
To use guix deploy, create a deployment configuration file that includes your target machine:
;; deploy.scm
(use-service-modules networking ssh)
(use-package-modules bootloaders ssh)
(define os (load "image.scm"))
(list (machine
(operating-system os)
(environment managed-host-environment-type)
(configuration (machine-ssh-configuration
(host-name "217.182.210.193")
(system "x86_64-linux")
(user "root")
(host-key #f)
(port 22)))))
This file defines a single machine managed through SSH.
To apply your configuration to the remote machine, simply run:
guix deploy deploy.scm
By default, Guix will build all required packages and system components locally, then transfer them to the server and activate the new configuration.
Extending Your Configuration
Now let's enhance our system by adding a user account, installing additional packages, and setting up a web server:
;; Updated image.scm with additional configuration
(operating-system
;; Previous configuration...
;; Add a non-root user
(users (cons* (user-account
(name "polyedre")
(comment "Polyedre")
(group "users")
(home-directory "/home/polyedre")
(supplementary-groups '("wheel" "netdev" "audio" "video")))
%base-user-accounts))
;; Add some useful packages
(packages (cons* git
curl
htop
%base-packages))
;; Services configuration
(services
(append
(list
(service dhcp-client-service-type)
(service openssh-service-type
(openssh-configuration
(openssh openssh-sans-x)
(permit-root-login #f) ;; Disable root login for security
(password-authentication? #f)
(authorized-keys
`(("polyedre" ,(local-file "/home/polyedre/.ssh/id_rsa.pub"))))))
;; Add cloud-init service
(service cloud-init-service-type
(cloud-init-configuration
(datasources '("ConfigDrive" "OpenStack" "NoCloud"))))
;; Add nginx web server
(service nginx-service-type
(nginx-configuration
(server-blocks
(list
(nginx-server-configuration
(listen '("80"))
(server-name '("www.polyed.re"))
(root (file-union "nginx-root"
`(("index.html" ,(local-file "./index.html"))))))))))
;; Add a basic firewall
(service iptables-service-type
(iptables-configuration
(ipv4-rules (plain-file "iptables.rules"
"
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -p tcp --dport 22 -j ACCEPT
-A INPUT -p tcp --dport 80 -j ACCEPT
-A INPUT -p tcp --dport 443 -j ACCEPT
COMMIT
"))
(ipv6-rules (plain-file "ip6tables.rules"
"
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -p ipv6-icmp -j ACCEPT
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -p tcp --dport 22 -j ACCEPT
-A INPUT -p tcp --dport 80 -j ACCEPT
-A INPUT -p tcp --dport 443 -j ACCEPT
COMMIT
")))))
(modify-services %base-services
(guix-service-type config =>
(guix-configuration
(inherit config)
(authorized-keys (append (list (local-file "/etc/guix/signing-key.pub")) %default-authorized-guix-keys))))))))
This extended configuration:
- Creates a non-root user with sudo privileges 2. Installs additional useful packages 3. Improves security by disabling root SSH login 4. Adds an Nginx web server 5. Configures a basic firewall to control inbound traffic
Deploy the updated configuration with guix deploy deploy.scm, and all these changes will be applied to your VM.
Security Hardening
Security is critical for any public-facing server. Here are some additional security measures to consider:
SSH Hardening
Update your openssh-configuration to include:
(openssh-configuration
(openssh openssh-sans-x)
(permit-root-login #f)
(password-authentication? #f)
(x11-forwarding? #f)
(allow-agent-forwarding? #f)
(max-auth-tries 3)
(log-level 'verbose)
(authorized-keys
`(("polyedre" ,(local-file "/home/polyedre/.ssh/id_rsa.pub")))))
Automatic Security Updates
Add a service to periodically update security-critical packages:
(service unattended-upgrade-service-type
(unattended-upgrade-configuration
(schedule "0 4 * * *"))) ;; Run at 4 AM daily
System Auditing
Enable system auditing with auditd:
(service auditd-service-type)
Advanced Configuration Examples
Setting Up PostgreSQL Database
Add a PostgreSQL database server to your VM:
(use-service-modules databases)
;; ...
(service postgresql-service-type
(postgresql-configuration
(port 5432)
(locale "en_US.utf8")
(create-user-groups? #t)
(authentication
(list (postgresql-authentication-rule
(auth-name "local")
(auth-method "peer")
(auth-matches '("all")))))
(extension-packages
(list postgresql-postgis))))
Setting Up a Reverse Proxy with Let's Encrypt
For secure web services with automatic TLS certificate management:
(use-modules (gnu services certbot))
;; ...
(service nginx-service-type
(nginx-configuration
(server-blocks
(list
(nginx-server-configuration
(server-name '("www.polyed.re"))
(listen '("443 ssl"))
(ssl-certificate "/etc/letsencrypt/live/www.polyed.re/fullchain.pem")
(ssl-certificate-key "/etc/letsencrypt/live/www.polyed.re/privkey.pem")
(root (file-union "nginx-root"
`(("index.html" ,(local-file "./index.html")))))
(locations
(list
(nginx-location-configuration
(uri "/")
(body '("try_files $uri $uri/ =404;")))))))
(server-tokens? #f)))
(service certbot-service-type
(certbot-configuration
(email "your.email@example.com")
(webroot "/var/www/certbot")
(certificates
(list
(certificate-configuration
(domains '("www.polyed.re"))
(deploy-hook "systemctl reload nginx"))))))
Troubleshooting
Common Issues and Solutions
VM Fails to Boot in OpenStack
- Issue: The VM shows as "Error" or "Shut Off" state in OpenStack. Solution: Check the console log for errors: ``
bash openstack console log show guix-1`` Common problems include incorrect bootloader configuration or filesystem issues. - Issue: Network connectivity fails in the VM. Solution: Ensure the DHCP client service is enabled and the network device is correctly configured. Check if the VM is receiving DHCP offers: ``
bash # Inside the VM ip addr journalctl -u dhclient``
SSH Connection Problems
- Issue: Cannot connect via SSH. Solution: Verify the security group allows port 22 and check if the SSH service is running: ``
bash # From your local machine openstack security group rule list default # Inside the VM (if you can access console) systemctl status sshd`` - Issue: SSH key authentication fails. Solution: Ensure the correct public key is in the authorized_keys file and permissions are correct: ``
bash # Inside the VM ls -la ~/.ssh/ chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys``
Guix Deploy Errors
- Issue: "Failed to connect to host" error during deployment. Solution: Verify the hostname/IP and SSH configuration in your deploy.scm file.
- Issue: Build failures during deployment. Solution: Try building the failing packages locally first to diagnose the issue: ``
bash guix build package-name``
Comparison with Other Tools
Guix Deploy vs. Ansible
| Feature | Guix Deploy | Ansible |
|---|---|---|
| Configuration Style | Declarative | Mostly procedural |
| Language | Guile Scheme | YAML + Jinja2 |
| Reproducibility | Very strong | Limited |
| Rollbacks | Built-in | Manual |
| Learning Curve | Steeper (requires Scheme) | Gentler |
| Ecosystem | Smaller | Larger |
Guix Deploy vs. Terraform + Configuration Management
While Terraform excels at provisioning cloud infrastructure, Guix Deploy focuses on system configuration. They can be complementary:
- Use Terraform to provision your OpenStack infrastructure
- Use Guix Deploy to configure the operating system and services
The main advantage of Guix is the unified approach—the same language and principles apply from package management to system configuration to deployment.
Conclusion
Deploying and managing Guix VMs in OpenStack offers a powerful, reproducible approach to cloud infrastructure. The declarative nature of Guix provides several key benefits:
- Reproducibility: Your entire system configuration is captured in code, making environments easy to reproduce 2. Transactional upgrades and rollbacks: System changes are atomic, and you can easily roll back if something goes wrong 3. Unified approach: The same principles and tools apply from package management to system configuration to deployment 4. Programmability: Using a full programming language (Guile Scheme) for configuration offers great flexibility
While there is a learning curve, particularly if you're not familiar with Scheme, the benefits are substantial for those who value reproducibility and a truly declarative approach to system management.
Guix may not be the most common choice for cloud deployments, but it offers a refreshing alternative to traditional tools. By combining the functional principles of Guix with the flexibility of cloud infrastructure, you can build systems that are both powerful and maintainable.