Beni's website

← Back to blog posts

I’ve been looking for a secure communications platform for a while, and I was very delighted when I found Matrix. It’s fully E2E encrypted, and what matters even more, is the feature of federation, meaning, that it’s designed for self-hosting, with the decentralized mesh of instances syncing and collaborating. If this is new to you, read more on the website

Choose your fighter

There are a bunch of Matrix server implementations out there, the official and recommended one is Synapse, however it has its shortcomings, the main one being system resource usage, since it’s written in Python, it’s not particularly fast or lightweight.

To avoid such bloat, I chose Conduit, and while it’s in beta, I’ve yet to run into any unfixable issues, it works pretty well just for me and my group of friends, but beware, the authors don’t yet recommend it for large public instances.

Installation

Once I’ve settled on Conduit, it was time to install it. If you’re not using NixOS, this section won’t be of much use, but the next one will still apply.

Conduit has it’s own documentation for NixOS (link), the installation of the server is dead simple using the built-in module.

Whilst you have the option to make your instance public allowing anyone to register, I’d highly advise against it, even if you have a beefy computer, because spammers will immediately find your server, and register a whole bunch of spam accounts on it, which is going to lead to your server getting blacklisted by every other matrix server, and even if you have the processing power, you simply won’t be able to talk to anyone outside your own server, which is kind of the whole point.

services.matrix-conduit = {
    enable = true;
    settings.global = {
        server_name = "example.com";
        address = "127.0.0.1";
        allow_registration = false; # change this if you want to allow random people to register
    };
};

Note: this will use the version of Conduit in nixpkgs, which is going to be pretty old if you don’t use the unstable version, in this case, I’d recommend using the Conduit flake to override to the latest version.

{
    # This will override it system-wide
    nixpkgs.overlays = [ (final: prev: { matrix-conduit = inputs.conduit.packages.${system}.conduit }) ];
    # This will only change the version the module uses
    services.matrix-conduit.package = inputs.conduit.packages.${system}.conduit;
}

Reverse proxy

Next up, is the reverse proxy configuration. I’d use nginx, since it’s easy to configure with nix options.

You may notice that I’m using the Quic-enabled version of nginx, and I’ve got http3 enabled everywhere, however feel free to just delete those lines to disable it, most clients don’t even use it.

{
    networking.firewall.allowedTCPPorts = [ 80 443 8448 ];
    networking.firewall.allowedUDPPorts = [ 80 443 8448 ]; # for Quic/http3

    # for ssl certs
    security.acme = {
        acceptTerms = true;
        defaults.email = "[email protected]";
    };

    services.nginx = {
        enable = true;
        package = pkgs.nginxQuic;
        recommendedOptimisation = true;
        recommendedProxySettings = true;
        recommendedTlsSettings = true;
        recommendedGzipSettings = true;
    };

    services.nginx.virtualHosts."matrix.example.com" = {
        forceSSL = true;
        enableACME = true;
        http3 = true;
        # listen on all interfaces on port 80 with http and ports 443, 8448 with https
        listen = builtins.map (port: { addr = "0.0.0.0"; inherit port; } // lib.optionalAttrs (port != 80) { ssl = true; }) [ 80 443 8448 ];
        locations."/_matrix/" = {
            proxyPass = "http://127.0.0.1:${toString config.services.matrix-conduit.settings.global.port}$request_uri";
            proxyWebsockets = true;
            extraConfig = ''
                proxy_set_header Host $host;
                proxy_buffering off;
            '';
        };
        # feel free to redirect the "home page" to your preferred client
        locations."/".return = "301 https://app.element.io";
        extraConfig = ''
            merge_slashes off;
        '';
    };
}

Subdomain configuration

That was basically it. Our matrix server is now up and running. There is just one small catch. You may have noticed, that in the nginx config we’ve set the domain up as matrix.example.com since you may want to have your root domain point to some different server. Your username will look like @user:matrix.example.com, but there’s a way to make it into a clean @user:example.com.

All we have to do, is tell Matrix how to find our real server when we give it just our root domain. Multiple docs recommended SRV DNS records, but I haven’t had too much success with those, as that would require that the server on our matrix subdomain present a valid SSL certificate for both the subdomain and the root domain. This is a real pain in the ass, as this would require you to point the root domain at this server, for the certificate verification. So instead, I’d recommend the other option, which is to return some JSON data on example.com/.well-known/matrix/.... Yes, on the root domain, and not the matrix one. This lookup also follows redirects, so all that’s needed is to set whatever service is running on your root domain to redirect this path to our matrix subdomain. For example, if you use Cloudflare, you can set up a simple redirect rule.

Cloudflare dashboard redirect rule

Now, we just have to make sure that our server responds with the correct JSON:

services.nginx.virtualHosts."matrix.example.com".locations."/.well-known/matrix" = {
    root = with pkgs; "" + symlinkJoin {
        name = "well-known-matrix";
        paths = [
            (writeTextDir ".well-known/matrix/server" ''{ "m.server": "matrix.example.com" }'')
            (writeTextDir ".well-known/matrix/client" ''{ "m.homeserver": { "base_url": "https://matrix.example.com" } }'')
        ];
    };
    # make sure nginx resolves the symlinks
    extraConfig = ''
        disable_symlinks off;
    '';
};

Conclusion

That’s all folks! I’ve yet to find a way to register the first admin account with registrations disabled, so the hack is to just enable the registrations, then open your client, or element if you don’t have one, register, then immediately turn registrations back off. Note, that in the homeserver url, you have to enter the full domain (ex. matrix.example.com), just the root won’t be enough, but don’t worry, it’ll be used everywhere else. Once you have your admin account, you’re going to get invited into an admin channel, where you can register your friends manually.