Bonfire & Guix, a love story

June 04, 2025
Tags:

Bonfire is a new framework to build federated applications that just reached RC1. It is written in Elixir, a nice functional language, and allows communities to make their own extensions to create custom flavored Fediverse applications, that can be tailored for the specific needs of different kinds of communities. I have been in touch with the core team and I'm trying to make the experience of running Bonfire on Guix as smooth as possible.

Guix is a general purpose provisioning tool, it implements trustable, functional and reproducible package recipes in the Guile language. It implements also a distro called Guix System, which provides transactional, atomic upgrades and can be completely be manipulated with Guile. This allows to easily modularize configuration bits and makes it possible to reuse or generate system configurations.

The post is structured in the following way:

Where you start from

In this post we'll set up a production Bonfire instance with a declarative approach over the Guix System. It will be a secure setup featuring: HTTPS, automatic backups and Guix provisioned secrets. This post also assumes you have a preinstalled Guix machine that you can login into either via console or SSH. If you need help with installing Guix, feel free to reach out, it takes time but I try to help everyone.

If, at any point while following the instructions, you get your system into a broken state you can always go back to the last known system generation with:

# Supposing the last known good generation ID is 120
sudo guix system switch-generation 120

Then reboot and you should be back again in a functional system. I recommend noting the current generation ID you are in before doing anything, with:

$ guix system list-generations

...

Generation 120  Jun 14 2025 20:57:49    (current)
  file name: /var/guix/profiles/system-120-link
  canonical file name: /gnu/store/lmg382z3zlpi9bcl81srd8dh92nr5aml-system
  label: GNU with Linux-Libre 6.14.11
  bootloader: grub
  root device: UUID: e4a319f6-6552-4d6b-afe8-9988df65173c
  kernel: /gnu/store/awmrxyh7i8phaqniwgmj7v4haxk8g9p2-linux-libre-6.14.11/bzImage
  channels:
    guix:
      repository URL: https://git.guix.gnu.org/guix.git
      branch: master
      commit: c8218094c47482c16f4cdd1e8092c35dab117418

In this case the generation ID is 120 (note the (current)).

DNS setup

Everything in this post assumes you have at least an A DNS record pointing to your machine's public IP address. Even better if you have also an AAAA record. Supposing your domain name is bonfire.fishinthecalculator.me you can check if DNS is working fine on the Guix System with:

$ guix shell bind bind:utils --  dig bonfire.fishinthecalculator.me A

The same goes for AAAA records:

$ guix shell bind bind:utils --  dig bonfire.fishinthecalculator.me AAAA

Enabling gocix

The first piece of configuration you'll need is the gocix channel in your user's .config/guix/channels.scm. gocix is a project I made to bring to your Guix installation cloud native applications, backed by OCI images, that can be configured in Guile. They have the most of the nice properties Guix native services have: atomic upgrades, transactions and rollbacks. After guix pull, in a new shell, your guix describe should look like this:

$ guix describe
Generation 68   Jun 13 2025 19:20:50    (current)
  guix 4c142ad
    repository URL: https://git.guix.gnu.org/guix.git
    branch: master
    commit: 4c142ad34b5ce32ce9004c3efa90d61d197ce436
  sops-guix 89f46bc
    repository URL: https://github.com/fishinthecalculator/sops-guix
    branch: main
    commit: 89f46bc4686504763f49e6b34c596720d347d8da
  gocix fca34c4
    repository URL: https://github.com/fishinthecalculator/gocix
    branch: main
    commit: fca34c4f871501b252d741fad0522a9fc65b65da

SSL certificates

You want to setup SSL certificates provisioning as soon as possible, since everything next presupposes HTTPS. To do so on the Guix system, you have to add the certbot-service-type to your operating-system record (which after installation is available in /etc/config.scm):

(use-modules (gnu services certbot) ...) ;for 'certbot-service-type'

(operating-system
  ...

  (services
    (list
      ...
      (service certbot-service-type
               (certbot-configuration
                (email "your@email.org")
                (certificates
                 (list
                  (certificate-configuration
                   (domains (list "bonfire.fishinthecalculator.me"))))))))))

Now, after running sudo guix system reconfigure /etc/config.scm, you should be able to check the status of the renew-certbot-certificates Shepherd service with:

$ sudo herd status renew-certbot-certificates
● Status of renew-certbot-certificates:
  It is stopped (one-shot).
  It is enabled.
  Provides: renew-certbot-certificates
  Requires: nginx
  Custom action: configuration
  Will be respawned.

Recent messages (use '-n' to view more or less):
  2025-03-19 22:32:18
  2025-03-19 22:32:18 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  2025-03-19 22:32:18 Certificate not yet due for renewal; no action taken.
  2025-03-19 22:32:18 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  2025-03-19 22:32:18 Certificate successfully acquired: bonfire.fishinthecalculator.me

The command shows that the service has successfully acquired an SSL certificate for your domain. From now on the Guix System will periodically take care of renewing the certificate. You can check the files are actually there with:

$ sudo ls -l /etc/letsencrypt/live/bonfire.fishinthecalculator.me/
total 4
lrwxrwxrwx 1 root root  54 Mar  8 02:51 cert.pem -> ../../archive/bonfire.fishinthecalculator.me/cert1.pem
lrwxrwxrwx 1 root root  55 Mar  8 02:51 chain.pem -> ../../archive/bonfire.fishinthecalculator.me/chain1.pem
lrwxrwxrwx 1 root root  59 Mar  8 02:51 fullchain.pem -> ../../archive/bonfire.fishinthecalculator.me/fullchain1.pem
lrwxrwxrwx 1 root root  57 Mar  8 02:51 privkey.pem -> ../../archive/bonfire.fishinthecalculator.me/privkey1.pem
-rw-r--r-- 1 root root 692 Mar  8 02:51 README

SSH assumptions

If you need SSH access you are encouraged to turn off password authentication and use only SSH keys. You can check the Guix manual on how to do that. Also fail2ban is pretty easy to setup with its default configuration for port 22.

Secrets setup

Next you are going to need to setup some secrets, both for the PostgreSQL database and the Bonfire instance. One way of doing so is with SOPS and sops-guix. SOPS allows for two different encyption tools: GPG and age. If you don't have any requirement the easiest way is to use age keys and encrypt secrets with them.

~$ sudo -i
Password: 
~# mkdir -p ~/.config/sops/age
~# guix shell age -- age-keygen -o /root/.config/sops/age/keys.txt
...

Public key: age1m3hcq7d9sl3d0uz6ezxvns4f7mjctksmmf5d8tpptmyz30rk9qnscgzfsa

You'll need one keypair for each user of the secret so, if you intend to be able to update secrets on a different machine than the one you are installing Bonfire on, make sure to generate a keypair there as well. Next you need to create a SOPS configuration file, named .sops.yaml, in the same directory your configuration file is:

keys:
    - &user_yourself_age age1peu96695en0xrlshkd3j3zzd04payh3cx27yjw6r40z8ekemnuesmkrupn
    - &host_yoursystem age1m3hcq7d9sl3d0uz6ezxvns4f7mjctksmmf5d8tpptmyz30rk9qnscgzfsa

creation_rules:
    - path_regex: .*yoursystem\.yaml$
      key_groups:
          - age:
                - *user_yourself_age
                - *host_yoursystem

You are now ready to create the secrets you need.

PostgreSQL secret

You will need a password to protect the PostgreSQL access from unauthorized users. You can generate a random string with:

$ guix shell openssl -- openssl rand -base64 32
v/hSYQHNCJMYW+U8D3m6ADQ+5382jN9iJ69gfImEISY=

From the same directory where the .sops.yaml and your configuration are stored, run the following command to create a yoursystem.yaml file that will store your encrypted secrets. Unencrypted secrets are supposed to never hit the disk, check out sops-guix README for more information.

$ guix shell sops -- sops yoursystem.yaml

Your default editor will pop up. Replace the SOPS example secrets and add the following content to the file:

postgres:
    bonfire: v/hSYQHNCJMYW+U8D3m6ADQ+5382jN9iJ69gfImEISY=

Save and close the editor. You can now check inside yoursystem.yaml and see that the secrets is effectively encrypted.

meilisearch secret

Bonfire requires an additional service to run searches, meilisearch. Generate another password and add, the same way as before with sops yoursystem.yaml, to your secrets file the following:

meilisearch:
    master: ...

Bonfire secrets

The last three secrets are for Bonfire. You can do that with a one-liner with:

$ sops set yoursystem.yaml '["bonfire"]' "$(echo '{"secret_key_base": "", "signing_salt": "", "encryption_salt": ""}'  | jq  ".secret_key_base = \"$(openssl rand -base64 128)\"" | jq  ".signing_salt = \"$(openssl rand -base64 128)\""  | jq  ".encryption_salt = \"$(openssl rand -base64 128)\"")

This should create correctly sized secrets for Bonfire.

Email secrets

Bonfire needs to be able to send emails to users, both for invites and password change. It supports many ways to do so, I happen to use the free tier of Mailjet but there are many options. Email setup is out of scope for this post, but at the end of the setup process you'll get one or more secret tokens. Add them in yoursystem.yaml the same way you did for the other secrets and save the file. Make sure to remember the list of keys in the YAML file necessary to access the secrets, in my case they are:

mail:
    bonfire:
        key: ...
        private_key: ...

When all secrets are in your yoursystem.yaml file your can add the following to your operating system configuration:

(use-modules (gnu services certbot) ;for 'certbot-service-type'
             (guix gexp)            ;for 'local-file'
             (guix utils)           ;for 'current-source-directory'
             (sops services sops)   ;for 'sops-secrets-service-type'
             (sops secrets)         ;for 'sops-secret'
             ...)

(define %project-root
 (current-source-directory))

(define sops.yaml
  (local-file (string-append %project-root "/.sops.yaml")
              "sops.yaml"))

(define-public yoursystem.yaml
  (local-file (string-append %project-root "/yoursystem.yaml")
              "yoursystem.yaml"))

;; PostgreSQL

(define-public bonfire-postgres-password-secret
  (sops-secret
   (key '("postgres" "bonfire"))
   (user "oci-container")
   (group "postgres")
   (file yoursystem.yaml)
   (permissions #o440)))

;; Meilisearch

(define-public meilisearch-key-secret
  (sops-secret
   (key '("meilisearch" "master"))
   (user "oci-container")
   (group "users")
   (file yoursystem.yaml)
   (permissions #o400)))

;; Bonfire

(define-public bonfire-mail-key-secret
  (sops-secret
   (key '("mail" "bonfire" "key"))
   (user "oci-container")
   (group "users")
   (file yoursystem.yaml)
   (permissions #o400)))
   
(define-public bonfire-mail-private-key-secret
  (sops-secret
   (key '("mail" "bonfire" "private_key"))
   (user "oci-container")
   (group "users")
   (file yoursystem.yaml)
   (permissions #o400)))

(define-public bonfire-secret-key-base-secret
  (sops-secret
   (key '("bonfire" "secret_key_base"))
   (user "oci-container")
   (group "users")
   (file yoursystem.yaml)
   (permissions #o400)))

(define-public bonfire-signing-salt-secret
  (sops-secret
   (key '("bonfire" "signing_salt"))
   (user "oci-container")
   (group "users")
   (file yoursystem.yaml)
   (permissions #o400)))

(define-public bonfire-encryption-salt-secret
  (sops-secret
   (key '("bonfire" "encryption_salt"))
   (user "oci-container")
   (group "users")
   (file yoursystem.yaml)
   (permissions #o400)))

(operating-system
  ...

  (services
    (list
      ...
      (service certbot-service-type
               ...)
      
      (service sops-secrets-service-type
               (sops-service-configuration
                (config sops.yaml))))))

Automatic database provisioning

Bonfire supports the PostgreSQL database engine and requires the postgis extension, this is what is is required to set them up:

(use-modules (gnu services certbot)    ;for 'certbot-service-type'
             (gnu services databases)  ;for 'postgresql-service-type' and 'postgresql-role-service-type'
             (gnu services networking) ;for 'iptables-service-type'
             (guix gexp)               ;for 'local-file'
             (guix utils)              ;for 'current-source-directory'
             (sops services sops)      ;for 'sops-secrets-service-type'
             (sops secrets)            ;for 'sops-secret'
             ...)


...

(operating-system
  ...

  (services
    (list
      ...
      (service certbot-service-type
               ...)
      
      (service sops-secrets-service-type
               ...)

      ;; Postgres
      (service postgresql-service-type
               (postgresql-configuration
                ;; Bonfire requires postgis. The latest version available on
                ;; Guix right now is 3.2.1, which is compatible with PostgreSQL
                ;; up to 15.
                (postgresql postgresql-15)
                (extension-packages (list postgis))
                (port 5432)))

      (service postgresql-role-service-type
               (postgresql-role-configuration
                (shepherd-requirement
                 ;; Allow database passwords to be provisioned through SOPS secrets 
                 (append
                  %default-postgresql-role-shepherd-requirement
                  '(sops-secrets))))))))

Firewall setup

The iptables-service-type is required by the rootless-podman-service-type, and in general in case the machine is exposed to the Internet having a firewall is a good idea. The following configuration allows only TCP or UDP connections on ports 22, 80 and 443. All ICMP traffic is dropped. All of this both for IPv4 and IPv6.

(use-modules (gnu services certbot)    ;for 'certbot-service-type'
             (gnu services databases)  ;for 'postgresql-service-type' and 'postgresql-role-service-type'
             (gnu services networking) ;for 'iptables-service-type'
             (guix gexp)               ;for 'local-file'
             (guix utils)              ;for 'current-source-directory'
             (sops services sops)      ;for 'sops-secrets-service-type'
             (sops secrets)            ;for 'sops-secret'
             ...)


...

(operating-system
  ...

  (services
    (list
      ...
      (service certbot-service-type
               ...)
      
      (service sops-secrets-service-type
               ...)

       ;; Postgres
      (service postgresql-service-type
               ...)

      (service postgresql-role-service-type
               ...)

      (service iptables-service-type
               (iptables-configuration
                (ipv4-rules (plain-file "iptables.rules" "*filter
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
-A INPUT -m conntrack --ctstate 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
-A INPUT -i lo -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-port-unreachable
COMMIT
"))
                (ipv6-rules (plain-file "ip6tables.rules" "*filter
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
-A INPUT -m conntrack --ctstate 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
-A INPUT -i lo -j ACCEPT
-A INPUT -j REJECT --reject-with icmp6-port-unreachable
COMMIT
")))))))

If the machine is not exposed to the internet then it is sufficient to add to your services:

(use-modules (gnu services certbot)    ;for 'certbot-service-type'
             (gnu services databases)  ;for 'postgresql-service-type' and 'postgresql-role-service-type'
             (gnu services networking) ;for 'iptables-service-type'
             (guix gexp)               ;for 'local-file'
             (guix utils)              ;for 'current-source-directory'
             (sops services sops)      ;for 'sops-secrets-service-type'
             (sops secrets)            ;for 'sops-secret'
             ...)

...

(operating-system
  ...

  (services
    (list
      ...
      (service certbot-service-type
               ...)
      
      (service sops-secrets-service-type
               ...)

       ;; Postgres
      (service postgresql-service-type
               ...)

      (service postgresql-role-service-type
               ...)

      (service iptables-service-type))))

Enabling rootless Podman

If you want to be able to run rootless containers with your own user follow the Guix manual, for the Bonfire OCI image to work it is sufficient to add the rootless-podman-service-type to your configuration:

(use-modules (gnu services certbot)    ;for 'certbot-service-type'
             (gnu services databases)  ;for 'postgresql-service-type' and 'postgresql-role-service-type'
             (gnu services containers) ;for 'rootless-podman-service-type'
             (gnu services dbus)       ;for 'dbus-service-type'
             (gnu services desktop)    ;for 'elogind-service-type'
             (gnu services networking) ;for 'iptables-service-type'
             (guix gexp)               ;for 'local-file'
             (guix utils)              ;for 'current-source-directory'
             (sops services sops)      ;for 'sops-secrets-service-type'
             (sops secrets)            ;for 'sops-secret'
             ...)

...

(operating-system
  ...

  (services
    (list
      ...
      (service certbot-service-type
               ...)
      
      (service sops-secrets-service-type
               ...)

       ;; Postgres
      (service postgresql-service-type
               ...)

      (service postgresql-role-service-type
               ...)

      (service iptables-service-type
               ...)

      ;; The DBus clique
      (service elogind-service-type)
      (service dbus-root-service-type)

      (service rootless-podman-service-type))))

Configuring OCI provisioning

Bonfire currently runs as an OCI image on the Guix System. This as an advantage that it's not needed to package and maintain Guix native packages for all the Elixir dependencies, and as a disadvantage that binaries in the OCI image can't be verifiably connected to the source code they are built from. In the past I did some effort for packaging Bonfire extensions but the work to build the Social flavor is still immense.

To run OCI services with the rootless-podman-service-type, you need to configure the oci-service-type:

(use-modules (gnu services certbot)    ;for 'certbot-service-type'
             (gnu services databases)  ;for 'postgresql-service-type' and 'postgresql-role-service-type'
             (gnu services containers) ;for 'rootless-podman-service-type'
             (gnu services dbus)       ;for 'dbus-service-type'
             (gnu services desktop)    ;for 'elogind-service-type'
             (gnu services networking) ;for 'iptables-service-type'
             (guix gexp)               ;for 'local-file'
             (guix utils)              ;for 'current-source-directory'
             (oci services containers) ;for 'oci-service-type'
             (sops services sops)      ;for 'sops-secrets-service-type'
             (sops secrets)            ;for 'sops-secret'
             ...)

...

(operating-system
  ...

  (services
    (list
      ...
      (service certbot-service-type
               ...)
      
      (service sops-secrets-service-type
               ...)

       ;; Postgres
      (service postgresql-service-type
               ...)

      (service postgresql-role-service-type
               ...)

      (service iptables-service-type
               ...)

      ;; The DBus clique
      (service elogind-service-type)
      (service dbus-root-service-type)

      (service rootless-podman-service-type)
      
      (service oci-service-type
               (oci-configuration
                (runtime 'podman))))))

Now reconfigure your system and reboot it to finalize service upgrades.

Starting Bonfire Social

To provision a functional Bonfire instance you need two services, meilisearch and the Bonfire flavour. The (oci services bonfire) module provides a Guix System service able to provision a database, SOPS secrets and the Bonfire instance. Add the following to your operating system configuration, and make sure to change the values based on your actual setup:

(use-modules (gnu services certbot)     ;for 'certbot-service-type'
             (gnu services databases)   ;for 'postgresql-service-type' and 'postgresql-role-service-type'
             (gnu services containers)  ;for 'rootless-podman-service-type'
             (gnu services dbus)        ;for 'dbus-service-type'
             (gnu services desktop)     ;for 'elogind-service-type'
             (gnu services networking)  ;for 'iptables-service-type'
             (guix gexp)                ;for 'local-file'
             (guix utils)               ;for 'current-source-directory'
             (oci services containers)  ;for 'oci-service-type'
             (oci services bonfire)     ;for 'oci-bonfire-service-type'
             (oci services meilisearch) ;for 'oci-meilisearch-service-type'
             (sops services sops)       ;for 'sops-secrets-service-type'
             (sops secrets)             ;for 'sops-secret'
             ...)

...

(operating-system
  ...

  (services
    (list
      ...
      (service certbot-service-type
               ...)
      
      (service sops-secrets-service-type
               ...)

       ;; Postgres
      (service postgresql-service-type
               ...)

      (service postgresql-role-service-type
               ...)

      (service iptables-service-type
               ...)

      ;; The DBus clique
      (service elogind-service-type)
      (service dbus-root-service-type)

      (service rootless-podman-service-type)
      
      (service oci-service-type
               ...)
      
      ;; meilisearch
      (service oci-meilisearch-service-type
               (oci-meilisearch-configuration
                ;; We use the host network since we have a firewall.
                ;; An OCI network could be used between Bonfire and meilisearch.
                (network "host")
                (port "7700")
                (master-key meilisearch-key-secret)))

      ;; Bonfire
      (service oci-bonfire-service-type
               (oci-bonfire-configuration
                (configuration
                 (bonfire-configuration
                  ;; Networking
                  (hostname "bonfire.fishinthecalculator.me")  ;replace with your domain
                  (port "4000")
                  (public-port "443")
                  (network "host")
                  ;; Postgres
                  (postgres-user "bonfire")
                  (postgres-db "bonfire")
                  ;; Mail
                  (mail-domain "bonfire.fishinthecalculator.me")                ;replace with your domain
                  (mail-from "friendlyadmin@bonfire.fishinthecalculator.me")))  ;replace with your domain
                  ;; Shepherd
                  (upload-data-directory "/var/lib/bonfire/uploads")
                  (auto-start? #t)
                  (requirement
                   '(postgresql postgres-roles sops-secrets podman-meilisearch))
                  (extra-variables
                   `(("MAIL_BACKEND" . "mailjet")
                     ("SERVER_PORT" . "4000")
                     ("SEARCH_MEILI_INSTANCE" . "http://localhost:7700")))
                  ;; Secrets
                  (meili-master-key
                   meilisearch-key-secret)
                  (postgres-password
                   bonfire-postgres-password-secret)
                  (mail-key
                   bonfire-mail-key-secret)
                  (mail-private-key
                   bonfire-mail-private-key-secret)
                  (secret-key-base
                   bonfire-secret-key-base-secret)
                  (signing-salt
                   bonfire-signing-salt-secret)
                  (encryption-salt
                   bonfire-encryption-salt-secret))))))

Now after reconfiguring your system, which might take a while, you should have a podman-bonfire service among your root's herd status:

$ sudo herd status podman-bonfire
Password: 
● Status of podman-bonfire:
  It is running since 11:48:45 AM (12 hours ago).
  Main PID: 26476
  Command: /run/current-system/profile/bin/podman run --rm --replace --name podman-bonfire --entrypoint /bin/sh --env POSTGRES_USER=bonfire --env FLAVOUR=social --env HOSTNAME=bonfire.fishinthecalculator.me --env POSTGRES_HOST=localhost --env POSTGRES_DB=bonfire --env MAIL_DOMAIN=bonfire.fishinthecalculator.me --env MAIL_FROM=friendlyadmin@bonfire.fishinthecalculator.me --env MAIL_PORT=465 --env MAIL_SSL=true --env PORT=4000 --env PUBLIC_PORT=443 --env LANG --env SEEDS_USER=root --env ERLANG_COOKIE=bonfire_cookie --env MIX_ENV=prod --env PLUG_BACKEND=bandit --env APP_NAME=Bonfire --env MAIL_BACKEND=mailjet --env SERVER_PORT=4000 --env SEARCH_MEILI_INSTANCE=http://localhost:7700 --network host -v /var/lib/bonfire/uploads:/opt/app/data/uploads -v /run/secrets/meilisearch:/run/secrets/meilisearch:ro -v /run/secrets/postgres:/run/secrets/postgres:ro -v /run/secrets/bonfire/mail:/run/secrets/bonfire/mail:ro -v /run/secrets/bonfire:/run/secrets/bonfire:ro docker.io/bonfirenetworks/bonfire:1.0.0-rc.1.10-social-amd64 -c "set -e; export MEILI_MASTER_KEY=\"$(cat /run/secrets/meilisearch/master)\"; export POSTGRES_PASSWORD=\"$(cat /run/secrets/postgres/bonfire)\"; export MAIL_KEY=\"$(cat /run/secrets/mail/bonfire/key)\"; export MAIL_PRIVATE_KEY=\"$(cat /run/secrets/mail/bonfire/private_key)\"; export SECRET_KEY_BASE=\"$(cat /run/secrets/bonfire/secret_key_base)\"; export SIGNING_SALT=\"$(cat /run/secrets/bonfire/signing_salt)\"; export ENCRYPTION_SALT=\"$(cat /run/secrets/bonfire/encryption_salt)\"; exec -a ./bin/bonfire ./bin/bonfire start"
  It is enabled.
  Provides: podman-bonfire
  Requires: postgresql postgres-roles sops-secrets podman-meilisearch cgroups2-fs-owner cgroups2-limits rootless-podman-shared-root-fs user-processes podman-volumes
  Custom actions: command-line pull
  Will not be respawned.
  Log file: /var/log/bonfire.log

Recent messages (use '-n' to view more or less):
  2025-06-13 23:45:44     Phoenix.Router.__call__/5 @ deps/phoenix/lib/phoenix/router.ex:475
  2025-06-13 23:45:44     Bonfire.Web.Endpoint.plug_builder_call/2 @ lib/bonfire/web/endpoint.ex:1

On the first run it will take some time as it will need to perform database migrations, in general you can check its output by reading /var/log/bonfire.log.

Running a reverse proxy

The last piece to be able to access your instance from the Internet is a reverse proxy. We'll use NGINX but any one should work. To configure NGINX to forward traffic to Bonfire the following must be added to your configuration:

(use-modules (gnu services certbot)     ;for 'certbot-service-type'
             (gnu services databases)   ;for 'postgresql-service-type' and 'postgresql-role-service-type'
             (gnu services containers)  ;for 'rootless-podman-service-type'
             (gnu services dbus)        ;for 'dbus-service-type'
             (gnu services desktop)     ;for 'elogind-service-type'
             (gnu services networking)  ;for 'iptables-service-type'
             (gnu services web)         ;for 'nginx-service-type'
             (guix gexp)                ;for 'local-file'
             (guix utils)               ;for 'current-source-directory'
             (oci services containers)  ;for 'oci-service-type'
             (oci services bonfire)     ;for 'oci-bonfire-service-type'
             (oci services meilisearch) ;for 'oci-meilisearch-service-type'
             (sops services sops)       ;for 'sops-secrets-service-type'
             (sops secrets)             ;for 'sops-secret'
             ...)

...

(operating-system
  ...

  (services
    (list
      ...
      (service certbot-service-type
               ...)
      
      (service sops-secrets-service-type
               ...)

       ;; Postgres
      (service postgresql-service-type
               ...)

      (service postgresql-role-service-type
               ...)

      (service iptables-service-type
               ...)

      ;; The DBus clique
      (service elogind-service-type)
      (service dbus-root-service-type)

      (service rootless-podman-service-type)
      
      (service oci-service-type
               ...)
      
      ;; meilisearch
      (service oci-meilisearch-service-type
               ...)

      ;; Bonfire
      (service oci-bonfire-service-type
               ...)

      (service nginx-service-type
               (nginx-configuration
                ;; Wait for bonfire to start
                (shepherd-requirement
                 '(podman-bonfire))
                (server-blocks
                 (list
                   (nginx-server-configuration
                    (server-name (list domain))
                    (listen '("443 ssl"))
                    ;; Replace with your domain
                    (ssl-certificate "/etc/certs/bonfire.fishinthecalculator.me/fullchain.pem")
                    ;; Replace with your domain
                    (ssl-certificate-key "/etc/certs/bonfire.fishinthecalculator.me/privkey.pem")
                    (locations
                     (list
                      (nginx-location-configuration
                       (uri "/")
                       (body (list (string-append "proxy_pass http://localhost:4000;")
                       ;; Taken from https://www.nginx.com/resources/wiki/start/topics/examples/full/
                       ;; Those settings are used when proxies are involved
                       "proxy_redirect          off;"
                       "proxy_set_header        Host $host;"
                       "proxy_set_header        X-Real-IP $remote_addr;"
                       "proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;"
                       "proxy_http_version      1.1;"
                       "proxy_cache_bypass      $http_upgrade;"
                       "proxy_set_header        Upgrade $http_upgrade;"
                       "proxy_set_header        Connection \"upgrade\";"
                       "proxy_set_header        X-Forwarded-Proto $scheme;"
                       "proxy_set_header        X-Forwarded-Host  $host;")))))))))))))

Now after reconfiguring you should be able to access your instance and be able to set it up. The first user to register is the admin, they can decide how further user can be added to the instance.

bonfire.fishinthecalculator.me's home page

Configuring database backups

If you'd like to have nightly jobs dumping Bonfire's database to disk, to be able to recover from data corruption for example, you can use the postgresql-backup-service-type. There is a PR for sending it upstream, but to use it now you'll have to add the small-guix channel to your .config/guix/channels.scm. After running guix pull, in a new shell, guix describe should mention small-guix.

Now you can add the following to your operating system configuration:

(use-modules (gnu services certbot)            ;for 'certbot-service-type'
             (gnu services databases)          ;for 'postgresql-service-type' and 'postgresql-role-service-type'
             (gnu services containers)         ;for 'rootless-podman-service-type'
             (gnu services dbus)               ;for 'dbus-service-type'
             (gnu services desktop)            ;for 'elogind-service-type'
             (gnu services networking)         ;for 'iptables-service-type'
             (gnu services web)                ;for 'nginx-service-type'
             (guix channels)                   ;for 'channel->code'
             (guix gexp)                       ;for 'local-file'
             (guix utils)                      ;for 'current-source-directory'
             (oci services containers)         ;for 'oci-service-type'
             (oci services bonfire)            ;for 'oci-bonfire-service-type'
             (oci services meilisearch)        ;for 'oci-meilisearch-service-type'
             (small-guix services databases)   ;for 'postgresql-backup-service-type'
             (sops services sops)              ;for 'sops-secrets-service-type'
             (sops secrets)                    ;for 'sops-secret'
             ...)

...

(operating-system
  ...

  (services
    (list
      ...
      (service certbot-service-type
               ...)
      
      (service sops-secrets-service-type
               ...)

       ;; Postgres
      (service postgresql-service-type
               ...)

      (service postgresql-role-service-type
               ...)
               
      (service postgresql-backup-service-type
               (postgresql-backup-configuration
                (package
                 ;; Use psql version 15.
                 (postgresql-backup-scripts/postgres postgresql-15))
                ;; Every day at 5 AM.
                (schedule "0 5 * * *")
                ;; Databases to backup.
                (databases '("bonfire")')
                ;; Which day to take the weekly backup from (1-7 = Monday-Sunday).
                (day-of-week-to-keep 6)
                ;; Number of days to keep daily backups.
                (days-to-keep 7)
                ;; How many weeks to keep weekly backups.
                (weeks-to-keep 5)))

      (service iptables-service-type
               ...)

      ;; The DBus clique
      (service elogind-service-type)
      (service dbus-root-service-type)

      (service rootless-podman-service-type)
      
      (service oci-service-type
               ...)
      
      ;; meilisearch
      (service oci-meilisearch-service-type
               ...)

      ;; Bonfire
      (service oci-bonfire-service-type
               ...)

      (service nginx-service-type
               ...))))

After some time, you'll get something like this at /var/lib/postgresql-backups:

$ sudo  tree -a /var/lib/postgresql-backups/
/var/lib/postgresql-backups/
├── 2025-06-05-daily
│   └── bonfire.custom
├── 2025-06-06-daily
│   └── bonfire.custom
├── 2025-06-07-weekly
│   └── bonfire.custom
├── 2025-06-08-daily
│   └── bonfire.custom
├── 2025-06-09-daily
│   └── bonfire.custom
├── 2025-06-10-daily
│   └── bonfire.custom
├── 2025-06-11-daily
│   └── bonfire.custom
└── 2025-06-12-daily
    └── bonfire.custom

You can then long term encrypt and backup these on S3, OneDrive, Google Drive with rclone and the restic-backup-service-type, check it out on the Guix Manual, search for restic-backup-service-type.

Enabling automatic upgrades

A good practice for a server exposed to the Internet is to continually apply security fixes. On Guix, for now, this means following the default branch. To automatically upgrade your system overnight, you can use the unattended-upgrade-service-type, configuring it to use gocix (and small-guix in case you use PostgreSQL backups) and to run at any given time. I run it once every night at 2AM:

(use-modules (gnu services admin)              ;for 'unattended-upgrade-service-type'
             (gnu services certbot)            ;for 'certbot-service-type'
             (gnu services databases)          ;for 'postgresql-service-type' and 'postgresql-role-service-type'
             (gnu services containers)         ;for 'rootless-podman-service-type'
             (gnu services dbus)               ;for 'dbus-service-type'
             (gnu services desktop)            ;for 'elogind-service-type'
             (gnu services networking)         ;for 'iptables-service-type'
             (gnu services web)                ;for 'nginx-service-type'
             (guix channels)                   ;for 'channel->code'
             (guix gexp)                       ;for 'local-file'
             (guix utils)                      ;for 'current-source-directory'
             (oci services containers)         ;for 'oci-service-type'
             (oci services bonfire)            ;for 'oci-bonfire-service-type'
             (oci services meilisearch)        ;for 'oci-meilisearch-service-type'
             (small-guix services databases)   ;for 'postgresql-backup-service-type'
             (sops services sops)              ;for 'sops-secrets-service-type'
             (sops secrets)                    ;for 'sops-secret'
             ...)

...

(define %channels
  (cons*
   (channel
    (name 'small-guix)
    (url "https://codeberg.org/fishinthecalculator/small-guix.git")
    (branch "main")
    ;; Enable signature verification:
    (introduction
     (make-channel-introduction
      "f260da13666cd41ae3202270784e61e062a3999c"
      (openpgp-fingerprint
       "8D10 60B9 6BB8 292E 829B  7249 AED4 1CC1 93B7 01E2"))))
   (channel
    (name 'gocix)
    (url "https://github.com/fishinthecalculator/gocix")
    (branch "main")
    ;; Enable signature verification:
    (introduction
    (make-channel-introduction
     "cdb78996334c4f63304ecce224e95bb96bfd4c7d"
     (openpgp-fingerprint
      "8D10 60B9 6BB8 292E 829B  7249 AED4 1CC1 93B7 01E2"))))
   %default-channels))

(operating-system
  ...

  (services
    (list
      ...
      (service certbot-service-type
               ...)
      
      (service sops-secrets-service-type
               ...)

       ;; Postgres
      (service postgresql-service-type
               ...)

      (service postgresql-role-service-type
               ...)
               
      (service postgresql-backup-service-type
               ...)

      (service iptables-service-type
               ...)

      ;; The DBus clique
      (service elogind-service-type)
      (service dbus-root-service-type)

      (service rootless-podman-service-type)
      
      (service oci-service-type
               ...)
      
      ;; meilisearch
      (service oci-meilisearch-service-type
               ...)

      ;; Bonfire
      (service oci-bonfire-service-type
               ...)

      (service nginx-service-type
               ...)

      (service unattended-upgrade-service-type
               (unattended-upgrade-configuration
                (schedule "0 2 * * *")
                 (system-expiration
                  ;; 30 days
                  (* 30 24 3600))
                 (channels
                  #~(list #$@(map channel->code %channels)))
                 (operating-system-file
                  ;; The path to your configuration file.
                  (file-append %project-root "/config.scm")))))))

After reconfiguring you should have a new Shepherd timer:


$ sudo herd status unattended-upgrade 
● Status of unattended-upgrade:
  It is running since Fri 13 Jun 2025 11:21:31 PM CEST (87 minutes ago).
  Timed service.
  Periodically running: /gnu/store/67mzs4f35yrbj224y2ygr73vy1s1iqis-unattended-upgrade
  It is enabled.
  Provides: unattended-upgrade
  Requires: user-processes networking
  Custom action: trigger
  Will be respawned.

Upcoming timer alarms:
  11:10:00 PM (in 22 hours)
  Sun 15 Jun 2025 11:10:00 PM CEST (in 46 hours)
  Mon 16 Jun 2025 11:10:00 PM CEST (in 3 days)
  Tue 17 Jun 2025 11:10:00 PM CEST (in 4 days)
  Wed 18 Jun 2025 11:10:00 PM CEST (in 5 days)

Enabling automatic reboots

Not all upgrades can be performed online and restarting sevices regularly sometimes helps. In case you want to regularly reboot your system you can use the unattended-reboot-service-type: it will provision a Shepherd timer which will reboot the system once triggered. This allows updates, for example for the Shepherd, the kernel or core packages, to be automatically finalized. In the future this should probably add an health check to see whether the system is functional and roll back otherwise.

To enable regular reboots add the following to your configuration:

(use-modules (gnu services admin)                      ;for 'unattended-upgrade-service-type'
             (gnu services certbot)                    ;for 'certbot-service-type'
             (gnu services databases)                  ;for 'postgresql-service-type' and 'postgresql-role-service-type'
             (gnu services containers)                 ;for 'rootless-podman-service-type'
             (gnu services dbus)                       ;for 'dbus-service-type'
             (gnu services desktop)                    ;for 'elogind-service-type'
             (gnu services networking)                 ;for 'iptables-service-type'
             (gnu services web)                        ;for 'nginx-service-type'
             (guix channels)                           ;for 'channel->code'
             (guix gexp)                               ;for 'local-file'
             (guix utils)                              ;for 'current-source-directory'
             (oci services containers)                 ;for 'oci-service-type'
             (oci services bonfire)                    ;for 'oci-bonfire-service-type'
             (oci services meilisearch)                ;for 'oci-meilisearch-service-type'
             (small-guix services databases)           ;for 'postgresql-backup-service-type'
             (small-guix services unattended-reboot)   ;for 'unattended-reboot-service-type'
             (sops services sops)                      ;for 'sops-secrets-service-type'
             (sops secrets)                            ;for 'sops-secret'
             ...)

...

(operating-system
  ...

  (services
    (list
      ...
      (service certbot-service-type
               ...)
      
      (service sops-secrets-service-type
               ...)

       ;; Postgres
      (service postgresql-service-type
               ...)

      (service postgresql-role-service-type
               ...)
               
      (service postgresql-backup-service-type
               ...)

      (service iptables-service-type
               ...)

      ;; The DBus clique
      (service elogind-service-type)
      (service dbus-root-service-type)

      (service rootless-podman-service-type)
      
      (service oci-service-type
               ...)
      
      ;; meilisearch
      (service oci-meilisearch-service-type
               ...)

      ;; Bonfire
      (service oci-bonfire-service-type
               ...)

      (service nginx-service-type
               ...)

      (service unattended-upgrade-service-type
               ...)

      (service unattended-reboot-service-type
               (unattended-reboot-configuration
                ;; Every day at 6AM.
                (schedule "0 6 * * *"))))))

Summing up

Let's try to see it all together:

(use-modules (gnu services admin)                      ;for 'unattended-upgrade-service-type'
             (gnu services certbot)                    ;for 'certbot-service-type'
             (gnu services databases)                  ;for 'postgresql-service-type' and 'postgresql-role-service-type'
             (gnu services containers)                 ;for 'rootless-podman-service-type'
             (gnu services dbus)                       ;for 'dbus-service-type'
             (gnu services desktop)                    ;for 'elogind-service-type'
             (gnu services networking)                 ;for 'iptables-service-type'
             (gnu services web)                        ;for 'nginx-service-type'
             (guix channels)                           ;for 'channel->code'
             (guix gexp)                               ;for 'local-file'
             (guix utils)                              ;for 'current-source-directory'
             (oci services containers)                 ;for 'oci-service-type'
             (oci services bonfire)                    ;for 'oci-bonfire-service-type'
             (oci services meilisearch)                ;for 'oci-meilisearch-service-type'
             (small-guix services databases)           ;for 'postgresql-backup-service-type'
             (small-guix services unattended-reboot)   ;for 'unattended-reboot-service-type'
             (sops services sops)                      ;for 'sops-secrets-service-type'
             (sops secrets)                            ;for 'sops-secret'
             ...)

(define %project-root
 (current-source-directory))

(define sops.yaml
  (local-file (string-append %project-root "/.sops.yaml")
              "sops.yaml"))

(define-public yoursystem.yaml
  (local-file (string-append %project-root "/yoursystem.yaml")
              "yoursystem.yaml"))

;; PostgreSQL

(define-public bonfire-postgres-password-secret
  (sops-secret
   (key '("postgres" "bonfire"))
   (user "oci-container")
   (group "postgres")
   (file yoursystem.yaml)
   (permissions #o440)))

;; Meilisearch

(define-public meilisearch-key-secret
  (sops-secret
   (key '("meilisearch" "master"))
   (user "oci-container")
   (group "users")
   (file yoursystem.yaml)
   (permissions #o400)))

;; Bonfire

(define-public bonfire-mail-key-secret
  (sops-secret
   (key '("mail" "bonfire" "key"))
   (user "oci-container")
   (group "users")
   (file yoursystem.yaml)
   (permissions #o400)))
   
(define-public bonfire-mail-private-key-secret
  (sops-secret
   (key '("mail" "bonfire" "private_key"))
   (user "oci-container")
   (group "users")
   (file yoursystem.yaml)
   (permissions #o400)))

(define-public bonfire-secret-key-base-secret
  (sops-secret
   (key '("bonfire" "secret_key_base"))
   (user "oci-container")
   (group "users")
   (file yoursystem.yaml)
   (permissions #o400)))

(define-public bonfire-signing-salt-secret
  (sops-secret
   (key '("bonfire" "signing_salt"))
   (user "oci-container")
   (group "users")
   (file yoursystem.yaml)
   (permissions #o400)))

(define-public bonfire-encryption-salt-secret
  (sops-secret
   (key '("bonfire" "encryption_salt"))
   (user "oci-container")
   (group "users")
   (file yoursystem.yaml)
   (permissions #o400)))

;; Channels
(define %channels
  (cons*
   (channel
    (name 'small-guix)
    (url "https://codeberg.org/fishinthecalculator/small-guix.git")
    (branch "main")
    ;; Enable signature verification:
    (introduction
     (make-channel-introduction
      "f260da13666cd41ae3202270784e61e062a3999c"
      (openpgp-fingerprint
       "8D10 60B9 6BB8 292E 829B  7249 AED4 1CC1 93B7 01E2"))))
   (channel
    (name 'gocix)
    (url "https://github.com/fishinthecalculator/gocix")
    (branch "main")
    ;; Enable signature verification:
    (introduction
    (make-channel-introduction
     "cdb78996334c4f63304ecce224e95bb96bfd4c7d"
     (openpgp-fingerprint
      "8D10 60B9 6BB8 292E 829B  7249 AED4 1CC1 93B7 01E2"))))
   %default-channels))

(operating-system
  ... ;; Bootloader, filesystems, users, packages, sudoers and so on...

  (services
    (list
      ... ;; Here you will most probably have to append to %base-sytem if this
          ;; is a headless machine
      (service certbot-service-type
               (certbot-configuration
                (email "your@email.org")  ;replace with your email
                (certificates
                 (list
                  (certificate-configuration
                   (domains (list "bonfire.fishinthecalculator.me"))))))) ;replace with your domain
      
      (service sops-secrets-service-type
               (sops-service-configuration
                (config sops.yaml)))

      ;; Postgres
      (service postgresql-service-type
               (postgresql-configuration
                ;; Bonfire requires postgis. The latest version available on
                ;; Guix right now is 3.2.1, which is compatible with PostgreSQL
                ;; up to 15.
                (postgresql postgresql-15)
                (extension-packages (list postgis))
                (port 5432)))

      (service postgresql-role-service-type
               (postgresql-role-configuration
                (shepherd-requirement
                 ;; Allow database passwords to be provisioned through SOPS secrets 
                 (append
                  %default-postgresql-role-shepherd-requirement
                  '(sops-secrets)))))
               
      (service postgresql-backup-service-type
               (postgresql-backup-configuration
                (package
                 ;; Use psql version 15.
                 (postgresql-backup-scripts/postgres postgresql-15))
                ;; Every day at 5 AM.
                (schedule "0 5 * * *")
                ;; Databases to backup.
                (databases '("bonfire")')
                ;; Which day to take the weekly backup from (1-7 = Monday-Sunday).
                (day-of-week-to-keep 6)
                ;; Number of days to keep daily backups.
                (days-to-keep 7)
                ;; How many weeks to keep weekly backups.
                (weeks-to-keep 5)))

      (service iptables-service-type
               (iptables-configuration
                (ipv4-rules (plain-file "iptables.rules" "*filter
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
-A INPUT -m conntrack --ctstate 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
-A INPUT -i lo -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-port-unreachable
COMMIT
"))
                (ipv6-rules (plain-file "ip6tables.rules" "*filter
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
-A INPUT -m conntrack --ctstate 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
-A INPUT -i lo -j ACCEPT
-A INPUT -j REJECT --reject-with icmp6-port-unreachable
COMMIT
"))))

      ;; The DBus clique
      (service elogind-service-type)
      (service dbus-root-service-type)

      (service rootless-podman-service-type)
      
      (service oci-service-type
               (oci-configuration
                (runtime 'podman)))
      
      ;; meilisearch
      (service oci-meilisearch-service-type
               (oci-meilisearch-configuration
                ;; We use the host network since we have a firewall.
                ;; An OCI network could be used between Bonfire and meilisearch.
                (network "host")
                (port "7700")
                (master-key meilisearch-key-secret)))

      ;; Bonfire
      (service oci-bonfire-service-type
               (oci-bonfire-configuration
                (configuration
                 (bonfire-configuration
                  ;; Networking
                  (hostname "bonfire.fishinthecalculator.me")  ;replace with your domain
                  (port "4000")
                  (public-port "443")
                  (network "host")
                  ;; Postgres
                  (postgres-user "bonfire")
                  (postgres-db "bonfire")
                  ;; Mail
                  (mail-domain "bonfire.fishinthecalculator.me")                ;replace with your domain
                  (mail-from "friendlyadmin@bonfire.fishinthecalculator.me")))  ;replace with your domain
                  ;; Shepherd
                  (upload-data-directory "/var/lib/bonfire/uploads")
                  (auto-start? #t)
                  (requirement
                   '(postgresql postgres-roles sops-secrets podman-meilisearch))
                  (extra-variables
                   `(("MAIL_BACKEND" . "mailjet") ;replace with your provider
                     ("SERVER_PORT" . "4000")
                     ("SEARCH_MEILI_INSTANCE" . "http://localhost:7700")))
                  ;; Secrets
                  (meili-master-key
                   meilisearch-key-secret)
                  (postgres-password
                   bonfire-postgres-password-secret)
                  (mail-key
                   bonfire-mail-key-secret)
                  (mail-private-key
                   bonfire-mail-private-key-secret)
                  (secret-key-base
                   bonfire-secret-key-base-secret)
                  (signing-salt
                   bonfire-signing-salt-secret)
                  (encryption-salt
                   bonfire-encryption-salt-secret)))

      ;; Reverse proxy
      (service nginx-service-type
               (nginx-configuration
                ;; Wait for bonfire to start
                (shepherd-requirement
                 '(podman-bonfire))
                (server-blocks
                 (list
                   (nginx-server-configuration
                    (server-name (list domain))
                    (listen '("443 ssl"))
                    ;; Replace with your domain
                    (ssl-certificate "/etc/certs/bonfire.fishinthecalculator.me/fullchain.pem")
                    ;; Replace with your domain
                    (ssl-certificate-key "/etc/certs/bonfire.fishinthecalculator.me/privkey.pem")
                    (locations
                     (list
                      (nginx-location-configuration
                       (uri "/")
                       (body (list (string-append "proxy_pass http://localhost:4000;")
                       ;; Taken from https://www.nginx.com/resources/wiki/start/topics/examples/full/
                       ;; Those settings are used when proxies are involved
                       "proxy_redirect          off;"
                       "proxy_set_header        Host $host;"
                       "proxy_set_header        X-Real-IP $remote_addr;"
                       "proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;"
                       "proxy_http_version      1.1;"
                       "proxy_cache_bypass      $http_upgrade;"
                       "proxy_set_header        Upgrade $http_upgrade;"
                       "proxy_set_header        Connection \"upgrade\";"
                       "proxy_set_header        X-Forwarded-Proto $scheme;"
                       "proxy_set_header        X-Forwarded-Host  $host;"))))))))))

      ;; Automatic upgrades.
      (service unattended-upgrade-service-type
               (unattended-upgrade-configuration
                ;; Every night at 2AM.
                (schedule "0 2 * * *")
                 (system-expiration
                  ;; 30 days
                  (* 30 24 3600))
                 (channels
                  #~(list #$@(map channel->code %channels)))
                 (operating-system-file
                  ;; The path to your configuration file.
                  (file-append %project-root "/config.scm"))))

      ;; Automatic reboots.
      (service unattended-reboot-service-type
               (unattended-reboot-configuration
                ;; Every day at 6AM.
                (schedule "0 6 * * *"))))))

This post has still many assumptions baked in which will require some experimentation on your side, if you have questions let me know. You can also look at the configuration for the machine running bonfire.fishinthecalculator.me.

Future work

Currently bonfire.fishinthecalculator.me runs pretty smoothly but the users are very few and the machine hosts other services. Next steps would probably be setup monitoring with Prometheus and Grafana for the VM and the Bonfire instance.