---
title: "Deploying Guix images on Linode"
slug: deploying-guix-images-on-linode
url: https://listedarticles.com/articles/deploying-guix-images-on-linode
canonical_url: https://dthompson.us/posts/deploying-guix-images-on-linode.html
content_type: blog_post
language: en
published_at: 2026-09-26T12:00:00.000Z
updated_at: 2026-09-27T03:09:52.732Z
author: "David Thompson"
author_url: https://dthompson.us
authored_by: human
publisher: "dthompson"
publisher_url: https://dthompson.us
topics: ["Linux", "Open Source", "Infrastructure"]
license: all-rights-reserved
word_count: 1000
reading_minutes: 4
citation: "David Thompson, dthompson. \"Deploying Guix images on Linode.\" 26 Sept 2026. https://dthompson.us/posts/deploying-guix-images-on-linode.html (all-rights-reserved)"
# The full text follows. The web page shows an extract and sends readers
# to the source above; quote the citation and link the canonical URL.
---

# Deploying Guix images on Linode

> David Thompson documents migrating to Linode (Akamai Cloud) and deploying Guix system images—image building, boot configuration, and practical notes from moving off DigitalOcean.

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.
