Matrix is an open standard that enables modern chat and voice and video. It is federated (like email) so you can run your own server and still communicate with others running their own servers or who have registered accounts with other servers. If you’d like to try it, check out Riot; a graphical client that can run in the browser, desktop, or on a mobile device.

Synapse is an open source server implementation of Matrix, and currently the standard server to use when setting up a Matrix homeserver. There are already packages and options in NixOS to setup Synapse.

We had been using Telegram as our sole chat service for about four years, however the fact that Telegram is a closed network (meaning you cannot run your own server) has been a growing concern for us. We care about our privacy and independence. Running our own Matrix homeserver allows chats between ourselves to stay on our server and frees us from being dependant on a provider for our chat service. Further, Matrix’s end-to-end encryption (still in beta) works between multiple users each having multiple devices, while Telegram’s end-to-end only works between one-to-one chats.

Before starting, I assume you already have a NixOS server and a domain name.

Setup DNS

You have a few options when setting up DNS. My preference is to have a subdomain like matrix.example.com point to my matrix server and then setup a SRV record so that our users full username will by @user:example.com instead of @user:matrix.example.com. You can have the base domain, example.com, point to the matrix server to get the same effect, but I like having the flexibility of using a subdomain.

My DNS records look like this:

1
2
_matrix._tcp.example.com. 1800 IN SRV 10 0 8448 matrix.example.com.
matrix 1800 IN A 1.2.3.4

You’ll need to replace example.com and the IPv4 address 1.2.3.4 with your own values. You could also use a AAAA record for IPv6. I did not use IPv6 since my host (currently Scaleway) does not provide IPv6 addresses that persist after reboot.

The SRV record is in this format:

1
_matrix._tcp.<yourdomain.com.> <ttl> IN SRV 10 0 <port> <synapse.server.name.>

The period (.) at the end of the domains is important to include if you are using the full domain name. Without the period at the end, the base domain name is assumed and appended. For example, matrix would translate to matrix.example.com if your base domain was example.com.

Setup PostgreSQL

Synapse supports using either SQLite (the default) or PostgreSQL. If you decide you are fine with using SQLite, you can skip this step.

Add the following snippet to configuration.nix:

1
2
3
{
  services.postgresql.enable = true;
}

Then apply the change by running:

1
$ sudo nixos-rebuild switch

This should start PostgreSQL. Next, you’ll want to create a user and a database for Synapse:

To connect to the PostgreSQL server, run:

1
$ sudo -u postgres psql

Execute the following SQL:

1
2
3
4
5
6
7
8
CREATE USER "matrix-synapse";

CREATE DATABASE "matrix-synapse"
    ENCODING 'UTF8'
    LC_COLLATE='C'
    LC_CTYPE='C'
    template=template0
    OWNER "matrix-synapse";

Then to quit psql, run:

1
postgres=# \q

I named the user and the database matrix-synapse since that is the default username the Synapse server will run as once we enable it and by default PostgreSQL uses the process owner to authenticate users.

Edit: After doing an update via nixos-rebuild switch --upgrade, the matrix-synapse service did not run. The reason is that an ExecStartPre= in the Systemd service that checks to see if a file at /var/lib/matrix-synapse/db-created exists and creates the matrix-synapse user and database (essentially what I did by hand). Creating an empty file where it expected solved this.

Configure Synapse

Configuring Synapse on NixOS is fairly easy. Many options you would set in in the homeserver.yaml file are available as NixOS options.

Add the following snippet to your configuration.nix file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  services.matrix-synapse = {
    enable = true;
    server_name = "example.com";
    registration_shared_secret = "secret";
    database_type = "psycopg2";
    database_args = {
      database = "matrix-synapse";
    };
    extraConfig = ''
      max_upload_size: "50M"
    '';
  };


  networking.firewall = {
    enable = true;
    allowedTCPPorts = [
      22    # SSH
      8448  # Matrix federation
    ];
  };
}

Replace the server_name with your own domain. The server_name option specifies the hostname other servers will use to connect to this server and the last part of user IDs.

Replace the registration_shared_secret with your own. You can generate one if you install pwgen and run pwgen -s 64 1. This option will allow you to register users via the command line later.

The database_type and database_args options configure Synapse to use the PostgreSQL database we setup earlier. If you prefer to use SQLite, these options can be omitted and a SQLite database will be stored as /var/lib/matrix-synapse/homeserver.db.

The extraConfig option allows you to specify arbitrary extra configuration that gets appended to the homeserver.yaml file after other configuration options. In my example, I set the max_upload_size to 50MB, allowing video files to be transfered that are up to 50MB. The default is 10MB and I found that users who had signed up with Matrix.org accounts could upload videos that were larger than 10MB but I couldn’t download them. After opening issue #2824 on GitHub a helpful developer let me know that the max_upload_size also determines the maximum download size via federation.

Once you are ready, run the following to apply the configuration changes:

1
$ sudo nixos-rebuild switch

You should be able to connect to your server via the URL https://matrix.example.com:8448/, if you replace example.com with your own domain. Your browser will alert you that you are navigating to an insecure site, but this is because Synapse uses self-signed certificates by default. You should be able to go into advanced options in your browser and add a temporary exception to continue to the login screen.

Registering a User

You can either open up user registration online or register users via the command-line. Since my server is a private server I only want to allow registration via the command-line.

Synapse comes with a script named register_new_matrix_user, but this won’t be found in your PATH after you enable the service. You have to find this in /nix/store and the easiest way I have found is to run systemctl status matrix-synapse which will output something similar to the following:

1
2
3
4
5
6
7
8
9
$ systemctl status matrix-synapse
● matrix-synapse.service - Synapse Matrix homeserver
   Loaded: loaded (/nix/store/g918vn57j2l1zviqgk9rgpy6id3qmn3n-unit-matrix-synapse.service/matrix-synapse.service; >
   Active: active (running) since Sun 2018-02-04 15:26:59 EST; 32min ago
  Process: 26139 ExecStartPre=/nix/store/hz7zsqjzz91ra5zh9sf0bvazb87pp4dc-unit-script/bin/matrix-synapse-pre-start >
 Main PID: 26222 (.homeserver-wra)
    Tasks: 7 (limit: 4915)
   CGroup: /system.slice/matrix-synapse.service
           └─26222 /nix/store/6yb5rvr6rvgvx8ylpchwz808djfw07rb-python-2.7.14/bin/python2.7 /nix/store/xh56g1s5abr9c>

In the Loaded: line you’ll find the path to the Systemd service file that is loaded for the server. In my case, it’s /nix/store/g918vn57j2l1zviqgk9rgpy6id3qmn3n-unit-matrix-synapse.service/matrix-synapse.service, but this will possibly be a different path for you. Read the contents of this file by running:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ cat /nix/store/g918vn57j2l1zviqgk9rgpy6id3qmn3n-unit-matrix-synapse.service/matrix-synapse.service
[Unit]
After=network.target
Description=Synapse Matrix homeserver

[Service]
# ...

ExecStart=/nix/store/xh56g1s5abr9c1r9q3qkjdba9j6d9fj5-matrix-synapse-0.26.0/bin/homeserver \
  --config-path /nix/store/b66rpj1jmnmgq6wgrbab32j51pxz0ldm-homeserver.yaml \
  --keys-directory /var/lib/matrix-synapse

# ...

You want to look at the ExecStart= within the [Service] section to find the directory where the process is being run for Synapse. The register_new_matrix_user script is kept in the same directory. You also want to find where the homeserver.yaml is kept so the registration script can read the registration_shared_secret from it.

In my case, I will run the following:

1
2
3
4
5
6
7
8
$ /nix/store/xh56g1s5abr9c1r9q3qkjdba9j6d9fj5-matrix-synapse-0.26.0/bin/register_new_matrix_user \
    -c /nix/store/b66rpj1jmnmgq6wgrbab32j51pxz0ldm-homeserver.yaml

New user localpart: myuser
Password:
Confirm password:
Make admin [no]:
Success!

The paths for you will likely be different since part of the path for each Nix derivative is a cryptographic hash. This is a part of how NixOS sandboxes dependencies on the filesystem.

If you want to enable users to create accounts on your server for themselves, you can set the following options:

1
2
3
4
5
6
7
8
{
  services.matrix-synapse = {
    # ...
    enable_registration = true;
    enable_registration_captcha = true;
    recaptcha_public_key = "<your public key>";
    recaptcha_private_key = "<your private key>";
}

This enables a ReCAPTCHA challenge to new registrants to prevent bots from creating accounts on your server. If you do not want a ReCAPTCHA challenge, you can omit the related options.

You should now be at a point where you can login to your server.

Setup Certificates with Let’s Encrypt

You can operate your server with self-signed certificates, but users will be presented with a pretty big warning from their browser and some clients, like Riot, won’t let you login without a signed certificate from a trusted certificate authority (CA). Thankfully we can get a free certificate from Let’s Encrypt with NixOS.

Update the options to your configuration.nix to be similar to the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
{
  services.postgresql.enable = true;

  services.matrix-synapse = {
    enable = true;
    server_name = "example.com";
    registration_shared_secret = "secret";
    public_baseurl = "https://matrix.example.com/";
    tls_certificate_path = "/var/lib/acme/matrix.example.com/fullchain.pem";
    tls_private_key_path = "/var/lib/acme/matrix.example.com/key.pem";
    database_type = "psycopg2";
    database_args = {
      database = "matrix-synapse";
    };
    listeners = [
      { # federation
        bind_address = "";
        port = 8448;
        resources = [
          { compress = true; names = [ "client" "webclient" ]; }
          { compress = false; names = [ "federation" ]; }
        ];
        tls = true;
        type = "http";
        x_forwarded = false;
      }
      { # client
        bind_address = "127.0.0.1";
        port = 8008;
        resources = [
          { compress = true; names = [ "client" "webclient" ]; }
        ];
        tls = false;
        type = "http";
        x_forwarded = true;
      }
    ];
    extraConfig = ''
      max_upload_size: "100M"
    '';
  };


  # web client proxy and setup certs
  services.nginx = {
    enable = true;
    virtualHosts = {
      "matrix.example.com" = {
        forceSSL = true;
        enableACME = true;
        locations."/" = {
          proxyPass = "http://127.0.0.1:8008";
        };
      };
    };
  };

  # share certs with matrix-synapse and restart on renewal
  security.acme.certs = {
    "matrix.example.com" = {
      group = "matrix-synapse";
      allowKeysForGroup = true;
      postRun = "systemctl reload nginx.service; systemctl restart matrix-synapse.service";
    };
  };

  networking.firewall = {
    enable = true;
    allowedTCPPorts = [
      22    # SSH
      8448  # Matrix federation
      80    # http
      443   # https
    ];
  };
}

Be sure to replace example.com with your own domain.

We use services.nginx to create a virtual host, matrix.example.com, and set enableACME to true. This will automatically fetch certs from Let’s Encrypt for that host and also sets up auto-renewal.

The security.acme.certs option is used to further configure what happens when we fetch certs. We allow the group matrix-synapse to read the certs and keys so the Synapse process can be configured with them, which is required for federation. We also set it up so that both Nginx and Synapse get restarted after certificate renewal.

For services.matrix-synapse the options tls_certificate_path and tls_private_key_path have been added with the paths to the certificate and key. We also opened up a new listener that only listens locally, provides the client, and does not use TLS on port 8008. Nginx is setup to proxy to this new listener and forces SSL. The default is to only listen on 8448, the federation port. Since we want to preserve this, the default configuration for the federation port listener is copied.

Update your server with the following command:

1
$ sudo nixos-rebuild switch

You should now be able to navigate to https://matrix.example.com/ without your browser warning you of an insecure connection. You can also now connect to your server with Riot. Select the Custom Server option when logging in and enter https://matrix.example.com/ as your Home server URL (leave the Identity server URL option alone).

Happy chatting!