DEV Community

Kir Axanov
Kir Axanov

Posted on

NixOS. Snikket. Self-host E2EE messenger with calls

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>
Enter fullscreen mode Exit fullscreen mode

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 - what compose2nix generated 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.
  };
}
Enter fullscreen mode Exit fullscreen mode

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
      };
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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.
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

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*
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)