2026.03.02
Hi!
Snikket is a modern federated messenger based on good ol' XMPP. It has attachments, group chats, audio / video messages and calls with end-to-end encryption and it is Docker / Podman ready.
If you too have a NixOS server and desperately searching for a self-hosted instant messenger with E2EE, calls and a high chance to keep your sanity during configuring and deploying... then continue reading (:
DNS
First of all, you should have a domain name.
My server holds a lot of stuff, so I host Snikket on a subdomain, like chat.example.com.
Besides chat.example.com you'll also need these two A-type DNS records (naming is up to you):
-
groups.chat.example.com- for all things about group chats -
share.chat.example.com- for files sharing
NixOS and OCI containers
To be short, OCI (Open Container Initiative) containers is what Docker has. Podman is a Docker replacement (DYOR what you should choose) and also works with OCI.
NixOS does not (yet) have a nix module for Snikket, however there is wonderful compose2nix project that turns any docker-compose.yml into nix file which you can import to your server's configuration (more about it later).
Configs
Configs tree:
<your other nixos configs>
├── nginx.nix
├── snikket.nix
├── snikket
│ ├── copy-certs-job.nix
│ ├── copy-certs.sh
│ ├── docker-compose.yml
│ ├── snikket.conf
│ └── snikket.nix
<your other nixos configs>
Where:
-
nginx.nix- general Nginx configuration shared between all deployed services on your server -
snikket.nix- all the specific configs for Snikket, other than Docker configuration -
snikket/docker-compose.yml- original Docker configuration for Snikket -
snikket/snikket.conf- environment file for Snikket Docker configuration -
snikket/snikket.nix- whatcompose2nixgenerated from Docker configuration -
snikket/copy-certs.sh- bash script to copy certificate files from Snikket container to Nginx directory -
snikket/copy-certs-job.nix- systemd timer configuration that executes said bash script daily
nginx.nix
{ pkgs, config, ... }:
{
networking.firewall.allowedTCPPorts = [ 80 443 ];
services.nginx = {
enable = true;
recommendedTlsSettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;
recommendedProxySettings = true;
appendHttpConfig = ''
proxy_headers_hash_max_size 1024;
proxy_headers_hash_bucket_size 128;
'';
resolver.ipv6 = false;
};
security.acme = {
acceptTerms = true;
defaults.email = "admin@example.com"; # <--- Change this.
};
}
snikket.nix
Look at Quick-start, Firewall ports and Advanced configuration for details.
For comments like Keep in sync with SNIKKET_DOMAIN see the snikket/snikket.conf for said env variable.
In allowedUDPPortRanges pay attention to port range, see How many ports does the TURN service need? section.
You can change client_max_body_size value in extraConfig = "client_max_body_size 104857616;"; to whatever suits you, I left the defaults.
{ lib, pkgs, config, ... }:
let
domain = "chat.example.com"; # Keep in sync with `SNIKKET_DOMAIN`.
groups_domain = "groups.${domain}";
share_domain = "share.${domain}";
port = 5443; # Keep in sync with `SNIKKET_TWEAK_HTTPS_PORT`.
in
{
imports = [
./snikket/snikket.nix
./snikket/copy-certs-job.nix
];
networking.firewall = {
allowedTCPPorts = [ 5222 5269 5000 3478 3479 5349 5350 ];
allowedUDPPorts = [ 3478 3479 5349 5350 ];
# Keep in sync with `SNIKKET_TWEAK_TURNSERVER_MIN_PORT` and `SNIKKET_TWEAK_TURNSERVER_MAX_PORT`.
allowedUDPPortRanges = [ {from = 49152; to = 49252;} ];
};
services.nginx = {
virtualHosts."${domain}" = {
enableACME = true;
forceSSL = true;
# Snikket manages ACME certs.
acmeFallbackHost = "localhost:${toString port}";
locations."/" = {
proxyPass = "https://localhost:${toString port}";
extraConfig = "client_max_body_size 104857616;"; # 100MB + 16 bytes
};
};
virtualHosts."${groups_domain}" = {
enableACME = true;
forceSSL = true;
# Snikket manages ACME certs.
acmeFallbackHost = "localhost:${toString port}";
locations."/" = {
proxyPass = "https://localhost:${toString port}";
extraConfig = "client_max_body_size 104857616;"; # 100MB + 16 bytes
};
};
virtualHosts."${share_domain}" = {
enableACME = true;
forceSSL = true;
# Snikket manages ACME certs.
acmeFallbackHost = "localhost:${toString port}";
locations."/" = {
proxyPass = "https://localhost:${toString port}";
extraConfig = "client_max_body_size 104857616;"; # 100MB + 16 bytes
};
};
};
}
snikket/docker-compose.yml
Download docker-compose.yml from Snikket.
Here's what it looks like (listing it for full coverage, I've changed nothing):
version: "3.3"
services:
snikket_proxy:
container_name: snikket-proxy
image: snikket/snikket-web-proxy:stable
env_file: snikket.conf
network_mode: host
volumes:
- snikket_data:/snikket
- acme_challenges:/var/www/html/.well-known/acme-challenge
restart: "unless-stopped"
snikket_certs:
container_name: snikket-certs
image: snikket/snikket-cert-manager:stable
network_mode: host
env_file: snikket.conf
volumes:
- snikket_data:/snikket
- acme_challenges:/var/www/.well-known/acme-challenge
restart: "unless-stopped"
snikket_portal:
container_name: snikket-portal
image: snikket/snikket-web-portal:stable
network_mode: host
env_file: snikket.conf
restart: "unless-stopped"
snikket_server:
container_name: snikket
image: snikket/snikket-server:stable
network_mode: host
volumes:
- snikket_data:/snikket
env_file: snikket.conf
restart: "unless-stopped"
volumes:
acme_challenges:
snikket_data:
snikket/snikket.conf
Be sure to keep in sync with snikket.nix.
# The primary domain of your Snikket instance.
SNIKKET_DOMAIN=chat.example.com
# An email address where the admin can be contacted
# (also used to register your Let's Encrypt account to obtain certificates).
SNIKKET_ADMIN_EMAIL=admin@example.com
# For reverse-proxy.
SNIKKET_TWEAK_HTTP_PORT=5080
SNIKKET_TWEAK_HTTPS_PORT=5443
SNIKKET_TWEAK_TURNSERVER_MIN_PORT=49152
SNIKKET_TWEAK_TURNSERVER_MAX_PORT=49252
snikket/snikket.nix
Generate this file with (run in snikket directory):
nix-shell -p compose2nix --run "compose2nix -inputs docker-compose.yml -output snikket.nix -project snikket"
You need to regenerate it in case docker-compose.yml was changed.
snikket/copy-certs.sh
Snikket handles ACME with its own service with certbot, so we need to copy those files to Nginx directory.
Also we need to rename key file and add a group read permissions to it.
#!/usr/bin/env bash
DOMAIN="chat.example.com"
SOURCE="/var/lib/containers/storage/volumes/snikket_snikket_data/_data/letsencrypt/live/$DOMAIN"
TARGET="/var/lib/acme/$DOMAIN"
cp -rL "$SOURCE" "$TARGET"
mv "$TARGET/privkey.pem" "$TARGET/key.pem"
chown -R acme:nginx "$TARGET"
chmod g+r "$TARGET/key.pem"
snikket/copy-certs-job.nix
Too lazy to figure out when to copy updated certs - just do it daily at 00:00 UTC:
# Periodic job that copies cert files from Snikket container to nginx directory.
{ config, pkgs, ... }:
{
systemd.timers."snikket-copy-certs" = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "5m";
OnCalendar = "*-*-* 00:00:00 UTC";
Unit = "snikket-copy-certs.service";
};
};
systemd.services."snikket-copy-certs" = {
path = with pkgs; [ bash ];
serviceConfig = {
Type = "oneshot";
ExecStart = "/path/to/copy-certs.sh"; # <--- Change this.
};
};
}
Running
Certs
Check that all Snikket services are up and ready (certs service can take a while to verify newly created certs):
systemctl status podman-snikket*
Replace podman-snikket* with docker-snikket* if you've chosen the Docker backend for compose2nix command.
Run snikket/copy-certs.sh. Now certs should be working.
Restart Nginx for good measure:
systemctl restart nginx.service
First admin user
To create your first user (who is admin), run this command and follow the invitation link:
docker exec snikket create-invite --admin --group default
All other users must join Snikket by an invite link, which you can create in admin panel at chat.example.com/admin/invitations. Invites can be one-time only and not restricted (also you can choose user role and circle).
Bye!
Top comments (0)