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
- DNS Setup
- Enabling
gocix
- Setting up SSL certificates (HTTPS) with Certbot
- SSH assumptions
- Secrets setup
- Automatic database provisioning
- Firewall setup
- Enabling rootless Podman
- Configuring OCI provisioning
- Starting Bonfire Social
- Running a Reverse proxy
- Configuring database backups
- Enabling automatic upgrades
- Enabling automatic reboots
- Summing up
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.
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.