Setting up a Matrix Homeserver on NixOS (Easy Edition)

Recently, a client suggested that I contact him via Matrix, a protocol with the goal of secure, decentralized communication. As my employer currently does not have host a Matrix Homeserver, I took the liberty of trying to set up my own.

Now, I’m no stranger to Matrix. My university set up a demo server around the time of the COVID pandemic. At the time, Matrix and its reference implementation server, Synapse, was a buggy, slow mess. The goal of achieving one common messenger protocol for everyone is a noble one, but also nontrivial to implement. Over the years, Matrix became more usable on my university’s server, culminating with a 2.0 release towards the end of my studies.

NixOS and Caddy make things easy

I love Caddy! Really. It is my go-to reverse proxy that just works. For most of my self-hosted applications, including Matrix, I can just declare a virtual host and pass the reverse_proxy directive. SSL, HTTP/3, websockets, everything is just there.

NixOS again simplifies the installation process, because I just declare the way I want things configured. This means that I can trivially connect Caddy to a Matrix homeserver. In this case, I chose tuwunel, which, despite the questionable name, seems to strike the best compromise between resource usage, stability and features.

All I had to do to get a basic configuration working was enable the services and supply configuration.

Caddy Configuration

The first part of my configuration involves setting up delegation, i.e. using a (root) domain for chatting @user:example.com with a different address, such as matrix.example.com, processing the requests. This is exactly what I did. redirecting any non-delegation requests to my personal home page.

{ config, pkgs, ... }:
let
  domain = config.networking.domain;
  matrixDomain = "matrix.${config.networking.domain}";
  port = toString config.services.matrix-tuwunel.settings.global.port;
in
{
  imports = [ ./caddy.nix ];

  services.caddy.virtualHosts = {
    ${domain} = {
      extraConfig = ''
        # Handle requests for delegation
        handle /.well-known/* {
          header /.well-known/matrix/* Content-Type application/json
          header /.well-known/matrix/* Access-Control-Allow-Origin *
          respond /.well-known/matrix/server `{"m.server": "${matrixDomain}:443"}`
          respond /.well-known/matrix/client `{"m.homeserver":{"base_url":"https://${matrixDomain}"}}`
        }

        # Redirect anything else to homepage.
        handle {
          redir https://johannes-arnold.de{uri}
        }
      '';
    };

    ${matrixDomain} = {
      extraConfig = ''
        reverse_proxy /_matrix/* 127.0.0.1:${port}
        reverse_proxy /_synapse/client/* 127.0.0.1${port}

        encode zstd gzip
        header X-Robots-Tag "noindex, nofollow"
        cache

        # Serve Element Web Interface
        root * ${pkgs.element-web}
        file_server
      '';
    };

  };
}

Note that the matrix.example.com also serves Element, a fully-featured Matrix chat client which can be served from a static page.

Now, I just need to set up my server of choice, which, in the case of Tuwunel, is remarkably simple as well:

services.matrix-tuwunel = {
  enable = true;
  settings = {
    global = {
      server_name = domain;
      allow_registration = true;
      trusted_servers = [
        "matrix.org"
        "matrix.uni-hannover.de"
      ];
      well_known = {
        client = "https://${matrixDomain}";
        server = "${matrixDomain}:443";
      };
    };
  };
};

Now, all that one needs to do to access the server is enable a registration token, navigate to their Matrix domain, and create an account.

Quote of the Day

Now a recipe is a lot like a computer program. A computer program’s a lot like a recipe: a series of steps to be carried out to get some result that you want. So it’s just as natural to do those same things with computer programs — hand a copy to your friend. Make changes in it because the job it was written to do isn’t exactly what you want. It did a great job for somebody else, but your job is a different job. And after you’ve changed it, that’s likely to be useful for other people. Maybe they have a job to do that’s like the job you do. So they ask, Hey, can I have a copy? Of course, if you’re a nice person, you’re going to give a copy. That’s the way to be a decent person.

— Richard Stallman


2025-08-15