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
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)