UP | HOME | RSS

Creating a universal chat server with Nix and Prosody

Table of Contents

Work in progress post

Intro

I'm working on creating a decentralized identity

Stack

  • nix
  • hertnzer
  • prosody

Why XMPP

I wanted something that is easy to self host and extend as needed. XMPP is a very old and well documented format so I have confidence it will be around for a long time, even if it might never get extremely popular. It's also fairly light, After using it for months my database is a 520kb sqlite file, prosody itself is idling at 90mb (which seems relatively high tbh), the IRC gateway is using 30mb and the matrix gateway is idling at 3mb

I considered Matrix but the setup seemed rather involved. Conduit looks promising though

XMPP Pain Points

The clients suck. The best I've been able to find is Gajim for my PC and Monal for my phone.

Initial Setup

I use https://nixos.org/ as much package manager on every platform. The amount of packages included makes experimenting with services really easy so I wanted to use a nixOS install as my

I went with a 2gb VPS on Hetzner and it's been working flawlessly.

To set up a basic nix OS follow the instructions here I didn't change much other than adjusting the file sizes, but just for reference:

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
  inputs.disko.url = "github:nix-community/disko";
  inputs.disko.inputs.nixpkgs.follows = "nixpkgs";
  inputs.deploy-rs.url = "github:serokell/deploy-rs";
  inputs.agenix.url = "github:ryantm/agenix";

  outputs =
    {
      self,
      nixpkgs,
      disko,
      deploy-rs,
      agenix,
    }:
    {
      nixosConfigurations.hetzner-cloud = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          disko.nixosModules.disko
          ./configuration.nix
          # Using agenix for secrets management
          agenix.nixosModules.default
          # Basic reverse proxy and SSL Configuration
          ./nginx.nix
          # Prosody Config
          ./xmpp.nix
          # Spoilers for a future article?
          ./gotosocial.nix
        ];
      };

      deploy.nodes.hertzner-cloud = {
        hostname = "public IP";
        profiles.system = {
          user = "root";
          sshUser = "root";
          path = deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.hetzner-cloud;
        };
      };
      checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
    };
}
# Example to create a bios compatible gpt partition
{ lib, ... }:
{
  disko.devices = {
    disk.disk1 = {
      device = lib.mkDefault "/dev/sda";
      type = "disk";
      content = {
        type = "gpt";
        partitions = {
          boot = {
            name = "boot";
            size = "1M";
            type = "EF02";
          };
          esp = {
            name = "ESP";
            size = "500M";
            type = "EF00";
            content = {
              type = "filesystem";
              format = "vfat";
              mountpoint = "/boot";
            };
          };
          root = {
            name = "root";
            size = "100%";
            content = {
              type = "lvm_pv";
              vg = "pool";
            };
          };
        };
      };
    };
    lvm_vg = {
      pool = {
        type = "lvm_vg";
        lvs = {
          root = {
            size = "100%FREE";
            content = {
              type = "filesystem";
              format = "ext4";
              mountpoint = "/";
              mountOptions = [
                "defaults"
              ];
            };
          };
        };
      };
    };
  };
}
{
  modulesPath,
  lib,
  pkgs,
  ...
}@args:
{
  imports = [
    (modulesPath + "/installer/scan/not-detected.nix")
    (modulesPath + "/profiles/qemu-guest.nix")
    ./disk-config.nix
  ];
  boot.loader.grub = {
    # no need to set devices, disko will add all devices that have a EF02 partition to the list already
    # devices = [ ];
    efiSupport = true;
    efiInstallAsRemovable = true;
  };

  services.openssh = {
    enable = true;
    settings = {
      PasswordAuthentication = false;
      KbdInteractiveAuthentication = false;
    };
  };

  services.fail2ban = {
    enable = true;
    bantime-increment.enable = true;
    jails = {
      nginx-http-auth.settings.enabled = true;
      nginx-botsearch.settings.enabled = true;
      nginx-bad-request.settings.enabled = true;
    };
  };

  environment.systemPackages = map lib.lowPrio [
    # Manual plugins todo
    pkgs.mercurial
    pkgs.curl
    pkgs.gitMinimal
    pkgs.openssl
    pkgs.dig
  ];

  time.timeZone = "UTC";

  users.users.root.openssh.authorizedKeys.keys = [
    # change this to your ssh key
    "MY KEY"
  ]
  ++ (args.extraPublicKeys or [ ]); # this is used for unit-testing this module and can be removed if not needed

  system.stateVersion = "24.05";

  virtualisation.containers.enable = true;

}

DNS Setup

Prosody requires you to configure several DNS records: an A record

The documentation here is fairly comprehensive

⚠️ I used namecheap for my domain which has proven itself to be quite annoying. One gotcha is the SRV Record GUI does not accurately show the values you type in, for example, typing in the host as _xmpp-server._tcp.conference.im will only show _xmpp-server._tcp in the UI

You can check if the values are set correctly through the following URL (replace example.com with your domain). I just used trial and error with manual dig calls until I got what I wanted

https://ap.www.namecheap.com/Domains/dns/GetAdvancedDnsInfo?domainName=example.com&fillTransferInfo=false

In the future Id like to move to a declarative setup with terraform as I did not anticipate just how many records Id have to make

For my setup Ive made records pointing to the top level domain as well as CNAME records for each one of the components I use, in my case

  • biboumi.example.com (IRC Gateway)
  • matridge.example.com (Matrix Gateway)
  • conference.example.com (Group XMPP Chats)
  • turn.example.com (Turn/Stun)
  • upload.example.com (File sharing through XMPP)

Author: neosloth

Created: 2026-05-07 Thu 20:25