Cached at:
09/27/26, 01:31 AM
# Deploying Guix images on Linode — dthompson
Source: [https://dthompson.us/posts/deploying-guix-images-on-linode.html](https://dthompson.us/posts/deploying-guix-images-on-linode.html)
Today, I find myself migrating from Digital Ocean to Linode \(now Akamai Cloud but I’m never gonna*really*call it that\) because, among other things, Digital Ocean recently[donated $3 million USD to Omarchy](https://www.digitalocean.com/blog/digitalocean-joins-omacom-foundation), a slop distro for fascists\. Raising money for free and open source software development is hard so it’s*pretty cool*when some guy[who doesn’t even write code anymore](https://jardo.dev/what-about-rails)gets dump trucks of money for nothing while honest projects compete for peanuts from small grant funding organizations\. But anyway\!
The Guix cookbook has[some documentation about running Guix on Linode](https://guix.gnu.org/cookbook/en/html_node/Running-Guix-on-a-Linode-Server.html)\. It takes an approach where you start with one of Linode’s built\-in Debian \([RIP](https://toot.cat/@dthompson/117322463609022181)\) images and then convert it to a Guix system\. As a former devops guy, I found this unsatisfying\. What I would really like is to upload a Guix image that is ready to go for use on Linode\. I’m gonna save the story and just say: I figured it out\.
My Linode disk image does the following:
- Allows use of virtual disks in the initial RAM disk
- Mounts the correct devices for`/`and swap
- Resizes the root file system upon boot to use all the space in the underlying Linode volume \(needed a custom service for this as Guix’s own`resize\-file\-system\-service`didn’t work for this\)
- Starts an SSH server that recognizes my public key
- Allows passwordless`sudo`so I can use`guix deploy`later
- Installs the bootloader files somewhere that Linode will recognize
- Gets an IPv6 address over DHCP in addition to its IPv4 address
Here’s the code I ended up with:
```
(define-module (dthompson linode)
#:use-module (gnu)
#:use-module (gnu packages linux)
#:use-module (gnu packages ssh)
#:use-module (gnu services)
#:use-module (gnu services admin)
#:use-module (gnu services networking)
#:use-module (gnu services shepherd)
#:use-module (gnu services ssh)
#:use-module (gnu system linux-initrd)
#:use-module (guix gexp)
#:use-module (guix modules)
#:use-module (guix profiles)
#:use-module (nongnu packages linux)
#:export (%linode-base-services
linode-base-os))
(define ssh-authorized-keys
`(("dave" ,(local-file "../keys/dave.pub"))))
(define guix-signing-keys
(list (local-file "../keys/signing-key.pub")))
;; Ideally we'd just use Guix's resize-file-system-service but it
;; makes some assumptions that do not hold for our Linode setup.
(define (resize2fs-shepherd-service device)
(list (shepherd-service
(provision '(resize2fs))
(requirement '(user-processes))
(one-shot? #f)
(respawn? #f)
(start (with-imported-modules (source-module-closure
'((guix build utils)))
#~(lambda _
(invoke #$(file-append e2fsprogs "/sbin/resize2fs")
#$device))))
(documentation "Resize ext filesystem on boot."))))
(define resize2fs-service-type
(service-type
(name 'resize2fs)
(extensions
(list (service-extension shepherd-root-service-type
resize2fs-shepherd-service)))
(default-value #f)
(description "Resize ext filesystem on boot")))
(define %linode-base-services
(cons*
(service dhcpcd-service-type
(dhcpcd-configuration
;; Linode servers get their IPv6 address via SLAAC and
;; the default setting of "private" doesn't work.
(slaac "hwaddr")))
;; SSH access via public/private key pairs only.
(service openssh-service-type
(openssh-configuration
(password-authentication? #f)
(authorized-keys ssh-authorized-keys)))
;; Automatically resize root filesystem to take up entire allocated
;; space.
(service resize2fs-service-type "/dev/sda")
;; Firewall that blocks nearly everything by default.
(service nftables-service-type)
(modify-services %base-services
;; Allow other Guix machines to push store items over via
;; 'guix deploy'.
(guix-service-type config =>
(guix-configuration
(inherit config)
(authorized-keys
(append guix-signing-keys
%default-authorized-guix-keys)))))))
(define linode-base-os
(operating-system
(locale "en_US.utf8")
(timezone "America/New_York")
(host-name "linode")
(users
(cons (user-account
(name "dave")
(comment "David Thompson")
(group "users")
(home-directory "/home/dave")
(supplementary-groups '("wheel")))
%base-user-accounts))
(sudoers-file
(plain-file "sudoers"
(string-append (plain-file-content %sudoers-specification)
;; 'guix deploy' requires no password
;; sudo capability.
"%wheel ALL=NOPASSWD: ALL\n")))
(packages (cons openssh %base-packages))
(services %linode-base-services)
;; Need virtio_scsi for using virtual disk devices.
(initrd-modules (append '("virtio_scsi") (base-initrd-modules linux)))
;; Guix's default behavior of running grub-install on the boot
;; device doesn't work in the Linode environment. Instead, we
;; just do what Guix does for disk images: Install the font and
;; GRUB modules to the root file system.
(bootloader
(bootloader-configuration
(bootloader
(bootloader
(inherit grub-bootloader)
(installer
#~(lambda (bootloader device mount-point)
(let* ((install-dir (string-append mount-point "/boot"))
(fonts (string-append install-dir "/grub/fonts")))
(mkdir-p fonts)
(copy-file (string-append bootloader "/share/grub/unicode.pf2")
(string-append fonts "/unicode.pf2"))
(copy-recursively (string-append bootloader "/lib/")
install-dir))))))
(targets '("/dev/sda"))))
(file-systems
(cons (file-system
(device "/dev/sda")
(mount-point "/")
(type "ext4"))
%base-file-systems))
(swap-devices (list (swap-space (target "/dev/sdb"))))))
;; Allow for building an initial disk image with 'guix system image'.
(when (batch-mode?) linode-base-os)
```
This code can also be found in[this Git repository](https://git.dthompson.us/guix-config/tree/dthompson/linode.scm)\.
Now this OS configuration needs to be turned into a usable disk image\. Guix can produce Linode\-compatible images using the`mbr\-raw`image type\. Linode images need to be gzip compressed before uploading\. I wrote a script to handle it:
```
#!/bin/sh
set -e
# First, we need to use Guix to generate a raw disk image that we can
# upload to Linode. Guix's 'mbr-raw' image type gets us most of the
# way there, but not quite. As stated in the name, these images have
# a master boot record. They also have GRUB installed in a post-MBR
# gap. After much trial and error we've discovered that what we want
# for Linode is an image that *only* contains the root partition.
image=$(guix system image -L . --image-type=mbr-raw dthompson/linode.scm)
# Guix places the root partition 1048576 bytes away from the beginning
# of the disk, so we need to skip over all of that when producing the
# final image.
#
# Somewhat arbitrarily, a 128K block size for 'dd' was chosen to speed
# up the operation vs. the default of 512 bytes. 8 blocks of 128K =
# 1048576 bytes, hence the 'skip=8' flag.
#
# Linode also requires uploaded images to be compressed with gzip.
dd if="$image" bs=128K skip=8 conv=sync,noerror | gzip -c > linode-base.gz
```
Once the disk image is created and uploaded, I can launch a new Linode instance using it\.*However*, Linode’s default configuration profile doesn’t work and the new instance simply kernel panics\. First, I stop the server\. Then I edit its configuration profile and make the following changes:
- Open the “Select a kernel” dropdown under “Boot Settings” and choose “GRUB 2”
- Turn off*all*of the filesystem/boot helper switches
Then I save and boot the server\. At this point I can use`guix deploy`over SSH for all OS updates\. And that’s it\!
It would probably be a good idea to update the Guix Cookbook with this improved process but I don’t have the energy for that right now so hopefully this blog post is helpful in the meanwhile\.