XMPP self-hosting tutorial

Fabio Natali, 23 February 2025

Intro

In this post we'll see how to install and configure Prosody, an open-source XMPP server. We will be deploying Prosody on a Hetzner cloud instance, provisioned and configured with Guix and the powerful guix deploy command.

XMPP and Prosody

XMPP is an open, extensible instant messaging protocol, originally introduced in 1999 and later formalised via a series of Internet Standard RFCs (RFC 3920, RFC 6120, RFC 6121, etc).

XMPP follows a client-server architecture, similarly to email. In order to use the protocol, you'll then need an account on an XMPP server and a client application on your devices, e.g. laptop and phone.

An XMPP account can be registered via one of the public providers (e.g. see this curated list). Alternatively, it's perfectly possible to self-host our own XMPP server, which is what we'll be doing here.

Guix Deploy and Hetzner

Guix is a Scheme-powered functional package manager and operating system that I might have mentioned once or twice in this blog already… ok, true, I talk about Guix all the time!

In this post we'll be leveraging a particular Guix command, guix deploy, that makes it possible to provision and install remote machines such as Virtual Private Servers (VPSes). Today we'll be using the Hetzner backend, hetzner-environment-type, which has been added to Guix only recently (thanks to Roman Scherer). The only other cloud integration available at the moment is for DigitalOcean, but in the future it should be possible to extend guix deploy to work with virtually any provider.

What pushed me to try Hetzner is their competitive pricing. However, creating an account was pretty frustrating and I got very close to abandon my attempt half the way through. What was putting me off was the amount of personal information collected during the process, especially when I was asked a copy of an ID document. Eventually my account was automagically verified without me having to send any ID. I was able to access my account regularly at that point and things have been working pretty well since then.

System definitions

We will break the installation into three steps:

  • provisioning of a base Guix system,
  • DNS configuration,
  • creation of TLS certificates and final setup of the XMPP server.

This can't be done in one go because we only get to know the instance IPv4 and IPv6 addresses after the initial provisioning.

We will need two Guix system definitions:

  • a system-base for the provisioning step,
  • a system-xmpp that inherits from system-base and includes Certbot and Prosody.

Let's save the definitions in systems.scm.

(define-module (systems)
  #:use-module (gnu)
  #:use-module (gnu packages bootloaders)
  #:use-module (gnu packages messaging)
  #:use-module (gnu services admin)
  #:use-module (gnu services certbot)
  #:use-module (gnu services messaging)
  #:use-module (gnu services networking)
  #:use-module (gnu services ssh)
  #:export (system-base
            system-xmpp))

(define %nftables-conf
  (plain-file
   "nftables.conf"
   "flush ruleset

table inet firewall {
    chain inbound {
        type filter hook input priority 0; policy drop;
        iif lo accept
        icmpv6 type {nd-neighbor-solicit, nd-router-advert,
                     nd-neighbor-advert} accept
        ct state vmap {established: accept, related: accept, invalid: drop}

        tcp dport http accept
        tcp dport https accept
        tcp dport ssh accept
        tcp dport xmpp-client accept
        tcp dport xmpp-server accept
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }
}"))

(define system-base
  (operating-system
    (host-name "xmpp")
    (timezone "Etc/UTC")
    (bootloader
     (bootloader-configuration
      (bootloader grub-bootloader)
      (targets (list "/dev/sda"))
      (terminal-outputs '(console))))
    (initrd-modules
     (cons "virtio_scsi" %base-initrd-modules))
    (file-systems (cons (file-system
                          (mount-point "/")
                          (device "/dev/sda1")
                          (type "ext4"))
                        %base-file-systems))

    (users (list (user-account
                  (name "alice")
                  (group "users")
                  (supplementary-groups '("wheel")))))
    (sudoers-file (plain-file
                   "sudoers"
                   (string-append
                    (plain-file-content %sudoers-specification)
                    "%wheel ALL = NOPASSWD: ALL")))

    (services
     (cons* (service dhcp-client-service-type)
            (service openssh-service-type
                     (openssh-configuration
                      (authorized-keys
                       `(("alice"
                          ,(plain-file "ssh.pub" "ssh-rsa AAAA..."))))
                      (permit-root-login 'prohibit-password)))
            (service unattended-upgrade-service-type)
            %base-services))))

(define system-xmpp
  (operating-system
    (inherit system-base)
    (packages (cons* prosody
                     (operating-system-packages system-base)))
    (services (cons*
               (service certbot-service-type
                        (certbot-configuration
                         (email "alice@example.com")
                         (certificates
                          (list
                           (certificate-configuration
                            (domains '("xmpp.example.com")))))))
               (service nftables-service-type
                        (nftables-configuration (ruleset %nftables-conf)))
               (service prosody-service-type
                        (prosody-configuration
                         (modules-enabled
                          (cons*
                           "groups" "mam" "carbons" %default-modules-enabled))
                         (virtualhosts
                          (list
                           (virtualhost-configuration
                            (domain "xmpp.example.com"))))))
               (operating-system-user-services system-base)))))

Machine definitions

We will be using guix deploy for the provisioning and configuration. We will need two machine definitions, one for the base system and one for the final installation. (In this context a machine definition is a Scheme record that includes relevant information about a server.) Let's save the machine definitions in machines.scm.

(define-module (machines)
  #:use-module (gnu)
  #:use-module (gnu machine)
  #:use-module (gnu machine hetzner)
  #:use-module (systems)
  #:export (machine-base
            machine-xmpp))

(define machine-base
  (machine
   (operating-system system-base)
   (environment hetzner-environment-type)
   (configuration (hetzner-configuration
                   (server-type "cx22")
                   (ssh-key "/home/alice/.ssh/id_rsa")))))

(define machine-xmpp
  (machine
   (inherit machine-base)
   (operating-system system-xmpp)))

server-type indicates the type of instance we want to use, e.g. in terms of CPU, RAM, and storage. The available server types, and respective costs, are listed here. For our use case, a relatively cheap and lightweight CX22 instance (2 VCPUs, 4GB RAM, 40GB SSD) should be fine. At the time of this writing, a CX22 comes at about 4 GBP/month.

Provisioning

Create an API token on Hetzner and set it as the environment variable GUIX_HETZNER_API_TOKEN. We can provision a VPS and install the initial system with:

guix deploy --load-path=. --expression='(list (@ (machines) machine-base))'

Under the hood, this involves quite a few steps and it may take a bit to complete. If all goes well, the process returns the IPv4 and IPv6 addresses of the provisioned machine. It should now be possible to access the machine via SSH, as alice and root.

Now it's time to pick a domain name for the XMPP server, e.g. xmpp.example.com, and configure it to point to the provisioned machine.

XMPP

Finally, we can install Certbot, request TLS certificates to Let's Encrypt, and install Prosody, the XMPP server. This is all done with one simple command:

guix deploy --load-path=. --expression='(list (@ (machines) machine-xmpp))'

Before being used, Prosody needs some configuration. Let's SSH into the server and run the following commands. First, the TLS certificates need to be imported into Prosody.

prosodyctl --root cert import /etc/certs

Second, create one or more XMPP users with:

prosodyctl adduser alice@xmpp.example.com

It should now be possible to configure a XMPP client with the alice@xmpp.example.com account. Job done!

My XMPP handle is fnat@xmpp.fnat.me, do reach out to me if you're an XMPP user.

Outro

As it turns out, I'm in a self-hosting spree as this is what my last few posts have been about. I'm glad that this time I've been able to use guix deploy, it makes the whole experience of provisioning and maintaining a cloud server so much more pleasant to me.

In particular, requesting TLS certificates with the Certbot service has become impressively easy - it's a blissful developer experience. The recently added hetzner-environment-type is also extremely pleasant to use. Big thanks to Roman, the person who contributed this environment type, for the original work on this and for helping me when I ran into a little problem.

Hope you've enjoyed this. I'll be Guixifing more and more of my cloud services in the future, so stay tuned!

Revision 760ac98.