Hosting a blog using only Scheme

August 31, 2020

I've discovered static blog generators using Hugo to write a travel blog a few years ago.

While it gets the job done, and allowed me to write articles using Org mode, I would not recommend it. I found it too complex for my modest needs.

Being quite involved in GNU Guix, I decided to have a look to Haunt that is currently used for the GNU Guix website.

Haunt defines itself as a simple, functional, hackable static site generator that gives authors the ability to treat websites as Scheme programs.

Writing the blog using Haunt

I chose to get started using the blog of David Thomson, the creator of Haunt itself. Haunt is well documented and writing a website mostly consists in creating a site object, this way:

(site #:title "Othacehe"
      #:domain "othacehe.org"
      #:build-directory "/tmp/website"
      #:default-metadata
      '((author . "Mathieu Othacehe")
        (email  . "othacehe@gnu.org"))
      #:readers (list commonmark-reader*)
      #:builders (list (blog #:theme my-theme
                             #:collections %collections)
                       (atom-feed)
                       (atom-feeds-by-tag)
                       index-page
                       projects-page
                       (static-directory "css")
                       (static-directory "fonts")
                       (static-directory "files")))

Pretty straightforward, compared to Hugo complex directory organization. The most problematical part for me here are the three letters CSS. My strong aversion for web design forbids me to roll-out my own theme.

One advantage of Hugo is the impressive collection of available website themes. Haunt being for now less popular, I opted for the exact same theme as David.

Then, a few lines of Scheme later, the website was ready. You can have a look to my haunt.scm. This very file contains the whole configuration of the website, all the rest is static content.

Building the website

The operation of building the website roughly consists in calling Haunt to convert the Scheme files and the post articles into a bunch of HTML files.

This can be done by invoking this command:

$ haunt build

To run this command you obviously need to have haunt installed, but also guile-syntax-highlight that is used for highlighting code snippets. As I'd like the building process to be self-contained, I use GNU Guix for this operation.

I won't go into details here but the idea is to write a .guix.scm file that specifies the build dependencies and the required command to build the website.

Then, all you have to do is:

$ guix build -f .guix.scm
...
build completed successfully
successfully built /gnu/store/19fx3njl6sr53nraayrvfzgb209g6mml-my-web-site.drv
/gnu/store/cgs02fcvmb3p7rni0nlb7wp85yd3mnkm-my-web-site

This returns a store directory that contains the built website. The only requirement here is to have GNU Guix available.

Now, we need to find a way to deploy this directory on a web server, and believe it or not, that's the fun part!

Deploying the website

I've been previously hosting stuff on a friend's server. I'd like to take my independence, but I don't want to use a machine at home, as I'm frequently moving.

This means that I need to find a hosting company. The choice is tricky because I don't want to run Dockers, Kubernetes or type a bunch of apt install commands and edit brutally some /etc/ configuration files.

I would prefer the hosted machine to run Guix System so that I can pursue my Scheme-only experiment.

Turns out GNU Guix has a deploy command that is able to:

The second option is very tempting, so I had a closer look at the mechanics. It appears that Guix System is not supported as a distribution by DigitalOcean. The employed trick for deployment is to spawn an Ubuntu VM, or droplet as it is fancily called, and install Guix System from there.

I'm not fond of this approach and I would prefer to have Guix System directly deployed. Luckily, I discovered that DigitalOcean supports running custom images, that's exactly what we need.

I decided to proceed this way:

Let's try it. We first need the os.scm configuration file. Here it is:

(use-modules (gnu)
             (sysadmin web))
(use-service-modules certbot networking ssh web)
(use-package-modules certs rsync screen ssh)

(define (cert-path host file)
  (format #f "/etc/letsencrypt/live/~a/~a.pem" host (symbol->string file)))

(operating-system
 (host-name "viso")
 (timezone "Europe/Paris")
 (locale "en_US.utf8")
 (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))
 (packages
  (cons* nss-certs openssh rsync screen
         %base-packages))
 (services
  (append
   (list
    (service certbot-service-type
             (certbot-configuration
              (email "othacehe@gnu.org")
              (certificates
               (list
                (certificate-configuration
                 (domains '("othacehe.org"))
                 (deploy-hook
                  (program-file
                   "nginx-deploy-hook"
                   #~(let ((pid (call-with-input-file "/var/run/nginx/pid" read)))
                       (kill pid SIGHUP)))))))))
    (service dhcp-client-service-type)
    (service nginx-service-type
             (nginx-configuration
              (server-blocks
               (list
                (nginx-server-configuration
                 (listen '("443 ssl" "[::]:443 ssl"))
                 (server-name (list "othacehe.org"))
                 (root "/var/www/website/")
                 (locations
                  (list
                   (nginx-location-configuration
                    (uri "/")
                    (body (list "index index.html;")))))
                 (ssl-certificate (cert-path "othacehe.org" 'fullchain))
                 (ssl-certificate-key (cert-path "othacehe.org" 'privkey)))))))
    (service openssh-service-type
             (openssh-configuration
              (openssh openssh-sans-x)
              (permit-root-login 'without-password)
              (authorized-keys
               `(("root"
                  ,(local-file "/home/mathieu/.ssh/id_rsa.pub"))))))
    (service static-web-site-service-type
             (static-web-site-configuration
              (git-url
               "https://gitlab.com/mothacehe/website.git")
              (directory "/var/www/website")
              (build-file ".guix.scm"))))
   %base-services)))

It is a bit lengthy, but keep in mind that it will allow us to run a custom Guix System distribution that behaves exactly as we want, out of the box.

Most of the work is to describe the five services we need:

You can have a look to the documentation here for the exact syntax of this file.

Then, we need to produce a disk-image out of this configuration file. It can be done that way:

$ guix system disk-image os.scm  -L ~/maintenance/hydra/modules/ --image-size=5G
...
/gnu/store/hca2zg51in1dnhaqkljfkvl8gyipmzni-disk-image

The extra -L option is required here because the static-web-site-service-type is not defined in GNU Guix itself, but in an external repository, available here. As stated previously, this service will create a cron job that fetches the website sources from my Gitlab repository, build them and deploy them. To save some extra space, let's also convert this raw disk-image into a compressed qcow2 image:

$ qemu-img convert -c -f raw -O qcow2 /gnu/store/hca2zg...-disk-image do.qcow2

The resulting image that weighs around 500MiB can then be uploaded to DigitalOcean, and a droplet using this custom image can be started from the web interface.

A few other manipulations are required to set up the DNS records, but nothing original.

That's it, using almost only Scheme, I was able to write and deploy this blog. This setup should be autonomous and the only work left is to write some articles, commit them, and wait for the droplet to fetch, build and deploy them.

If for some reason I need to edit the configuration of the droplet in the future, I have two options:

The two options are equally valid but the second one should go faster as it does not involve uploading a new image. Anyway, we will maybe explore those options in a future article.

Stay tuned.