{"article":{"slug":"deploying-guix-images-on-linode","title":"Deploying Guix images on Linode","subtitle":null,"summary":"David Thompson documents migrating to Linode (Akamai Cloud) and deploying Guix system images—image building, boot configuration, and practical notes from moving off DigitalOcean.","content_type":"blog_post","language":"en","canonical_url":"https://dthompson.us/posts/deploying-guix-images-on-linode.html","author":{"name":"David Thompson","url":"https://dthompson.us","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"dthompson","url":"https://dthompson.us","listing_slug":null,"listing":null},"topics":[{"name":"Linux","slug":"linux","url":"https://listedarticles.com/topics/linux"},{"name":"Open Source","slug":"open-source","url":"https://listedarticles.com/topics/open-source"},{"name":"Infrastructure","slug":"infrastructure","url":"https://listedarticles.com/topics/infrastructure"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1000,"reading_minutes":4,"published_at":"2026-09-26T12:00:00.000Z","added_at":"2026-09-27T03:09:52.732Z","updated_at":"2026-09-27T03:09:52.732Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/deploying-guix-images-on-linode","markdown_url":"https://listedarticles.com/articles/deploying-guix-images-on-linode.md","example":false,"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)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://dthompson.us/posts/deploying-guix-images-on-linode.html"},"body_markdown":"Today, I find myself migrating from Digital Ocean to Linode (now\nAkamai Cloud but I’m never gonna *really* call it that) because, among\nother things, Digital Ocean recently [donated $3 million USD to\nOmarchy](https://www.digitalocean.com/blog/digitalocean-joins-omacom-foundation),\na slop distro for fascists.  Raising money for free and open source\nsoftware development is hard so it’s *pretty cool* when some guy [who\ndoesn’t even write code anymore](https://jardo.dev/what-about-rails)\ngets dump trucks of money for nothing while honest projects compete\nfor peanuts from small grant funding organizations.  But anyway!\n\nThe Guix cookbook has [some documentation about running Guix on\nLinode](https://guix.gnu.org/cookbook/en/html_node/Running-Guix-on-a-Linode-Server.html).\nIt takes an approach where you start with one of Linode’s built-in\nDebian ([RIP](https://toot.cat/@dthompson/117322463609022181)) images\nand then convert it to a Guix system.  As a former devops guy, I found\nthis unsatisfying.  What I would really like is to upload a Guix image\nthat is ready to go for use on Linode.  I’m gonna save the story and\njust say: I figured it out.\n\nMy Linode disk image does the following:\n\n- Allows use of virtual disks in the initial RAM disk\n- Mounts the correct devices for `/` and swap\n- 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)\n- Starts an SSH server that recognizes my public key\n- Allows passwordless `sudo` so I can use`guix deploy` later\n- Installs the bootloader files somewhere that Linode will recognize\n- Gets an IPv6 address over DHCP in addition to its IPv4 address\n\nHere’s the code I ended up with:\n\n```\n(define-module (dthompson linode)\n  #:use-module (gnu)\n  #:use-module (gnu packages linux)\n  #:use-module (gnu packages ssh)\n  #:use-module (gnu services)\n  #:use-module (gnu services admin)\n  #:use-module (gnu services networking)\n  #:use-module (gnu services shepherd)\n  #:use-module (gnu services ssh)\n  #:use-module (gnu system linux-initrd)\n  #:use-module (guix gexp)\n  #:use-module (guix modules)\n  #:use-module (guix profiles)\n  #:use-module (nongnu packages linux)\n  #:export (%linode-base-services\n            linode-base-os))\n(define ssh-authorized-keys\n  `((\"dave\" ,(local-file \"../keys/dave.pub\"))))\n(define guix-signing-keys\n  (list (local-file \"../keys/signing-key.pub\")))\n;; Ideally we'd just use Guix's resize-file-system-service but it\n;; makes some assumptions that do not hold for our Linode setup.\n(define (resize2fs-shepherd-service device)\n  (list (shepherd-service\n          (provision '(resize2fs))\n          (requirement '(user-processes))\n          (one-shot? #f)\n          (respawn? #f)\n          (start (with-imported-modules (source-module-closure\n                                         '((guix build utils)))\n                   #~(lambda _\n                       (invoke #$(file-append e2fsprogs \"/sbin/resize2fs\")\n                               #$device))))\n          (documentation \"Resize ext filesystem on boot.\"))))\n(define resize2fs-service-type\n  (service-type\n    (name 'resize2fs)\n    (extensions\n     (list (service-extension shepherd-root-service-type\n                              resize2fs-shepherd-service)))\n    (default-value #f)\n    (description \"Resize ext filesystem on boot\")))\n(define %linode-base-services\n  (cons*\n   (service dhcpcd-service-type\n            (dhcpcd-configuration\n              ;; Linode servers get their IPv6 address via SLAAC and\n              ;; the default setting of \"private\" doesn't work.\n              (slaac \"hwaddr\")))\n   ;; SSH access via public/private key pairs only.\n   (service openssh-service-type\n            (openssh-configuration\n              (password-authentication? #f)\n              (authorized-keys ssh-authorized-keys)))\n   ;; Automatically resize root filesystem to take up entire allocated\n   ;; space.\n   (service resize2fs-service-type \"/dev/sda\")\n   ;; Firewall that blocks nearly everything by default.\n   (service nftables-service-type)\n   (modify-services %base-services\n     ;; Allow other Guix machines to push store items over via\n     ;; 'guix deploy'.\n     (guix-service-type config =>\n                        (guix-configuration\n                          (inherit config)\n                          (authorized-keys\n                           (append guix-signing-keys\n                                   %default-authorized-guix-keys)))))))\n(define linode-base-os\n  (operating-system\n    (locale \"en_US.utf8\")\n    (timezone \"America/New_York\")\n    (host-name \"linode\")\n    (users\n     (cons (user-account\n             (name \"dave\")\n             (comment \"David Thompson\")\n             (group \"users\")\n             (home-directory \"/home/dave\")\n             (supplementary-groups '(\"wheel\")))\n           %base-user-accounts))\n    (sudoers-file\n     (plain-file \"sudoers\"\n                 (string-append (plain-file-content %sudoers-specification)\n                                ;; 'guix deploy' requires no password\n                                ;; sudo capability.\n                                \"%wheel ALL=NOPASSWD: ALL\\n\")))\n    (packages (cons openssh %base-packages))\n    (services %linode-base-services)\n    ;; Need virtio_scsi for using virtual disk devices.\n    (initrd-modules (append '(\"virtio_scsi\") (base-initrd-modules linux)))\n    ;; Guix's default behavior of running grub-install on the boot\n    ;; device doesn't work in the Linode environment.  Instead, we\n    ;; just do what Guix does for disk images: Install the font and\n    ;; GRUB modules to the root file system.\n    (bootloader\n      (bootloader-configuration\n        (bootloader\n          (bootloader\n            (inherit grub-bootloader)\n            (installer\n             #~(lambda (bootloader device mount-point)\n                 (let* ((install-dir (string-append mount-point \"/boot\"))\n                        (fonts (string-append install-dir \"/grub/fonts\")))\n                   (mkdir-p fonts)\n                   (copy-file (string-append bootloader \"/share/grub/unicode.pf2\")\n                              (string-append fonts \"/unicode.pf2\"))\n                   (copy-recursively (string-append bootloader \"/lib/\")\n                                     install-dir))))))\n        (targets '(\"/dev/sda\"))))\n    (file-systems\n     (cons (file-system\n             (device \"/dev/sda\")\n             (mount-point \"/\")\n             (type \"ext4\"))\n           %base-file-systems))\n    (swap-devices (list (swap-space (target \"/dev/sdb\"))))))\n;; Allow for building an initial disk image with 'guix system image'.\n(when (batch-mode?) linode-base-os)\n```\nThis code can also be found in [this Git\nrepository](https://git.dthompson.us/guix-config/tree/dthompson/linode.scm).\n\nNow this OS configuration needs to be turned into a usable disk image.\nGuix can produce Linode-compatible images using the `mbr-raw` image\ntype.  Linode images need to be gzip compressed before uploading.  I\nwrote a script to handle it:\n\n```\n#!/bin/sh\nset -e\n# First, we need to use Guix to generate a raw disk image that we can\n# upload to Linode.  Guix's 'mbr-raw' image type gets us most of the\n# way there, but not quite.  As stated in the name, these images have\n# a master boot record.  They also have GRUB installed in a post-MBR\n# gap.  After much trial and error we've discovered that what we want\n# for Linode is an image that *only* contains the root partition.\nimage=$(guix system image -L . --image-type=mbr-raw dthompson/linode.scm)\n# Guix places the root partition 1048576 bytes away from the beginning\n# of the disk, so we need to skip over all of that when producing the\n# final image.\n#\n# Somewhat arbitrarily, a 128K block size for 'dd' was chosen to speed\n# up the operation vs. the default of 512 bytes.  8 blocks of 128K =\n# 1048576 bytes, hence the 'skip=8' flag.\n#\n# Linode also requires uploaded images to be compressed with gzip.\ndd if=\"$image\" bs=128K skip=8 conv=sync,noerror | gzip -c > linode-base.gz\n```\nOnce the disk image is created and uploaded, I can launch a new Linode\ninstance using it.  *However*, Linode’s default configuration profile\ndoesn’t work and the new instance simply kernel panics.  First, I stop\nthe server.  Then I edit its configuration profile and make the\nfollowing changes:\n\n- Open the “Select a kernel” dropdown under “Boot Settings” and choose “GRUB 2”\n- Turn off *all* of the filesystem/boot helper switches\n\nThen I save and boot the server.  At this point I can use `guix deploy` over SSH for all OS updates.  And that’s it!\n\nIt 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.","body_html":"<p>Today, I find myself migrating from Digital Ocean to Linode (now\nAkamai Cloud but I’m never gonna <em>really</em> call it that) because, among\nother things, Digital Ocean recently <a href=\"https://www.digitalocean.com/blog/digitalocean-joins-omacom-foundation\" rel=\"nofollow ugc noopener\">donated $3 million USD to\nOmarchy</a>,\na slop distro for fascists.  Raising money for free and open source\nsoftware development is hard so it’s <em>pretty cool</em> when some guy <a href=\"https://jardo.dev/what-about-rails\" rel=\"nofollow ugc noopener\">who\ndoesn’t even write code anymore</a>\ngets dump trucks of money for nothing while honest projects compete\nfor peanuts from small grant funding organizations.  But anyway!</p>\n<p>The Guix cookbook has <a href=\"https://guix.gnu.org/cookbook/en/html_node/Running-Guix-on-a-Linode-Server.html\" rel=\"nofollow ugc noopener\">some documentation about running Guix on\nLinode</a>.\nIt takes an approach where you start with one of Linode’s built-in\nDebian (<a href=\"https://toot.cat/@dthompson/117322463609022181\" rel=\"nofollow ugc noopener\">RIP</a>) images\nand then convert it to a Guix system.  As a former devops guy, I found\nthis unsatisfying.  What I would really like is to upload a Guix image\nthat is ready to go for use on Linode.  I’m gonna save the story and\njust say: I figured it out.</p>\n<p>My Linode disk image does the following:</p>\n<ul><li>Allows use of virtual disks in the initial RAM disk</li><li>Mounts the correct devices for <code>/</code> and swap</li><li>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 <code>resize-file-system-service</code> didn’t work for this)</li><li>Starts an SSH server that recognizes my public key</li><li>Allows passwordless <code>sudo</code> so I can use<code>guix deploy</code> later</li><li>Installs the bootloader files somewhere that Linode will recognize</li><li>Gets an IPv6 address over DHCP in addition to its IPv4 address</li></ul>\n<p>Here’s the code I ended up with:</p>\n<pre><code>(define-module (dthompson linode)\n  #:use-module (gnu)\n  #:use-module (gnu packages linux)\n  #:use-module (gnu packages ssh)\n  #:use-module (gnu services)\n  #:use-module (gnu services admin)\n  #:use-module (gnu services networking)\n  #:use-module (gnu services shepherd)\n  #:use-module (gnu services ssh)\n  #:use-module (gnu system linux-initrd)\n  #:use-module (guix gexp)\n  #:use-module (guix modules)\n  #:use-module (guix profiles)\n  #:use-module (nongnu packages linux)\n  #:export (%linode-base-services\n            linode-base-os))\n(define ssh-authorized-keys\n  `((&quot;dave&quot; ,(local-file &quot;../keys/dave.pub&quot;))))\n(define guix-signing-keys\n  (list (local-file &quot;../keys/signing-key.pub&quot;)))\n;; Ideally we&#39;d just use Guix&#39;s resize-file-system-service but it\n;; makes some assumptions that do not hold for our Linode setup.\n(define (resize2fs-shepherd-service device)\n  (list (shepherd-service\n          (provision &#39;(resize2fs))\n          (requirement &#39;(user-processes))\n          (one-shot? #f)\n          (respawn? #f)\n          (start (with-imported-modules (source-module-closure\n                                         &#39;((guix build utils)))\n                   #~(lambda _\n                       (invoke #$(file-append e2fsprogs &quot;/sbin/resize2fs&quot;)\n                               #$device))))\n          (documentation &quot;Resize ext filesystem on boot.&quot;))))\n(define resize2fs-service-type\n  (service-type\n    (name &#39;resize2fs)\n    (extensions\n     (list (service-extension shepherd-root-service-type\n                              resize2fs-shepherd-service)))\n    (default-value #f)\n    (description &quot;Resize ext filesystem on boot&quot;)))\n(define %linode-base-services\n  (cons*\n   (service dhcpcd-service-type\n            (dhcpcd-configuration\n              ;; Linode servers get their IPv6 address via SLAAC and\n              ;; the default setting of &quot;private&quot; doesn&#39;t work.\n              (slaac &quot;hwaddr&quot;)))\n   ;; SSH access via public/private key pairs only.\n   (service openssh-service-type\n            (openssh-configuration\n              (password-authentication? #f)\n              (authorized-keys ssh-authorized-keys)))\n   ;; Automatically resize root filesystem to take up entire allocated\n   ;; space.\n   (service resize2fs-service-type &quot;/dev/sda&quot;)\n   ;; Firewall that blocks nearly everything by default.\n   (service nftables-service-type)\n   (modify-services %base-services\n     ;; Allow other Guix machines to push store items over via\n     ;; &#39;guix deploy&#39;.\n     (guix-service-type config =&gt;\n                        (guix-configuration\n                          (inherit config)\n                          (authorized-keys\n                           (append guix-signing-keys\n                                   %default-authorized-guix-keys)))))))\n(define linode-base-os\n  (operating-system\n    (locale &quot;en_US.utf8&quot;)\n    (timezone &quot;America/New_York&quot;)\n    (host-name &quot;linode&quot;)\n    (users\n     (cons (user-account\n             (name &quot;dave&quot;)\n             (comment &quot;David Thompson&quot;)\n             (group &quot;users&quot;)\n             (home-directory &quot;/home/dave&quot;)\n             (supplementary-groups &#39;(&quot;wheel&quot;)))\n           %base-user-accounts))\n    (sudoers-file\n     (plain-file &quot;sudoers&quot;\n                 (string-append (plain-file-content %sudoers-specification)\n                                ;; &#39;guix deploy&#39; requires no password\n                                ;; sudo capability.\n                                &quot;%wheel ALL=NOPASSWD: ALL\\n&quot;)))\n    (packages (cons openssh %base-packages))\n    (services %linode-base-services)\n    ;; Need virtio_scsi for using virtual disk devices.\n    (initrd-modules (append &#39;(&quot;virtio_scsi&quot;) (base-initrd-modules linux)))\n    ;; Guix&#39;s default behavior of running grub-install on the boot\n    ;; device doesn&#39;t work in the Linode environment.  Instead, we\n    ;; just do what Guix does for disk images: Install the font and\n    ;; GRUB modules to the root file system.\n    (bootloader\n      (bootloader-configuration\n        (bootloader\n          (bootloader\n            (inherit grub-bootloader)\n            (installer\n             #~(lambda (bootloader device mount-point)\n                 (let* ((install-dir (string-append mount-point &quot;/boot&quot;))\n                        (fonts (string-append install-dir &quot;/grub/fonts&quot;)))\n                   (mkdir-p fonts)\n                   (copy-file (string-append bootloader &quot;/share/grub/unicode.pf2&quot;)\n                              (string-append fonts &quot;/unicode.pf2&quot;))\n                   (copy-recursively (string-append bootloader &quot;/lib/&quot;)\n                                     install-dir))))))\n        (targets &#39;(&quot;/dev/sda&quot;))))\n    (file-systems\n     (cons (file-system\n             (device &quot;/dev/sda&quot;)\n             (mount-point &quot;/&quot;)\n             (type &quot;ext4&quot;))\n           %base-file-systems))\n    (swap-devices (list (swap-space (target &quot;/dev/sdb&quot;))))))\n;; Allow for building an initial disk image with &#39;guix system image&#39;.\n(when (batch-mode?) linode-base-os)</code></pre>\n<p>This code can also be found in <a href=\"https://git.dthompson.us/guix-config/tree/dthompson/linode.scm\" rel=\"nofollow ugc noopener\">this Git\nrepository</a>.</p>\n<p>Now this OS configuration needs to be turned into a usable disk image.\nGuix can produce Linode-compatible images using the <code>mbr-raw</code> image\ntype.  Linode images need to be gzip compressed before uploading.  I\nwrote a script to handle it:</p>\n<pre><code>#!/bin/sh\nset -e\n# First, we need to use Guix to generate a raw disk image that we can\n# upload to Linode.  Guix&#39;s &#39;mbr-raw&#39; image type gets us most of the\n# way there, but not quite.  As stated in the name, these images have\n# a master boot record.  They also have GRUB installed in a post-MBR\n# gap.  After much trial and error we&#39;ve discovered that what we want\n# for Linode is an image that *only* contains the root partition.\nimage=$(guix system image -L . --image-type=mbr-raw dthompson/linode.scm)\n# Guix places the root partition 1048576 bytes away from the beginning\n# of the disk, so we need to skip over all of that when producing the\n# final image.\n#\n# Somewhat arbitrarily, a 128K block size for &#39;dd&#39; was chosen to speed\n# up the operation vs. the default of 512 bytes.  8 blocks of 128K =\n# 1048576 bytes, hence the &#39;skip=8&#39; flag.\n#\n# Linode also requires uploaded images to be compressed with gzip.\ndd if=&quot;$image&quot; bs=128K skip=8 conv=sync,noerror | gzip -c &gt; linode-base.gz</code></pre>\n<p>Once the disk image is created and uploaded, I can launch a new Linode\ninstance using it.  <em>However</em>, Linode’s default configuration profile\ndoesn’t work and the new instance simply kernel panics.  First, I stop\nthe server.  Then I edit its configuration profile and make the\nfollowing changes:</p>\n<ul><li>Open the “Select a kernel” dropdown under “Boot Settings” and choose “GRUB 2”</li><li>Turn off <em>all</em> of the filesystem/boot helper switches</li></ul>\n<p>Then I save and boot the server.  At this point I can use <code>guix deploy</code> over SSH for all OS updates.  And that’s it!</p>\n<p>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.</p>","headings":[]}}