Table of Contents

Setting up a Synapse + Element Matrix chat server


Firewall

The following ports have to be opened in your firewall:

80/tcp   # HTTP
443/tcp  # HTTPS


Domain setup

You will have to decide if you want Synapse to use your root domain or a subdomain, and on which subdomain you want to serve Element.

Examples are:

It doesn't really matter which you choose, but configuration will depend on it slightly. In this case, I decided to use quietlife.nl as the homeserver domain and chat.quietlife.nl to serve Element.

Note that Element is merely a web application talking to Synapse. It is not required to build a Matrix server. You can also use Synapse without Element, using locally installed Matrix client applications instead. So you are free to leave out Element entirely if you wish.



Docker

Unfortunately, Synapse and Element are not packaged for Debian, so you can't simply apt install them. For now, Docker is likely the easiest way to deploy this stack.

It is probably a good idea to first read Setting up IPv6 NAT in Docker in order to have IPv6 working for these containers.


Compose file

First create /var/opt/matrix:

sudo mkdir /var/opt/matrix

Then create docker-compose.yml there:

/var/opt/matrix/docker-compose.yml
services:
  postgres:
    image: postgres:17
    restart: always
    environment:
      POSTGRES_USER: "synapse"
      POSTGRES_PASSWORD: "5yN@Ps3"
      POSTGRES_DB: "synapse"
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "synapse"]
    networks:
      - network
    volumes:
      - /var/opt/matrix/postgres:/var/lib/postgresql/data

  synapse:
    image: matrixdotorg/synapse:latest
    restart: always
    depends_on:
      - postgres
    networks:
      - network
    ports:
      - "127.0.0.1:8008:8008"
      - "[::1]:8008:8008"
    volumes:
      - /var/opt/matrix/synapse:/data

Make sure to change POSTGRES_PASSWORD to something you generated yourself.


If you also want to serve Element, add this:

/var/opt/matrix/docker-compose.yml
  element:
    image: vectorim/element-web:latest
    restart: always
    networks:
      - network
    ports:
      - "127.0.0.1:8088:80"
      - "[::1]:8088:80"
    volumes:
      - /var/opt/matrix/element/config.json:/app/config.json


If you have an IPv6-enabled Docker setup, add this:

/var/opt/matrix/docker-compose.yml
networks:
  network:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
        - subnet: 2001:db8:db8:8008::/64


PostgreSQL

Start PostgreSQL:

cd /var/opt/matrix
sudo docker compose up -d postgres

The database created by PostgreSQL's init script upsets Synapse because of the collation.

So after starting PostgreSQL, drop the database and recreate it manually:

sudo docker exec -it matrix-postgres-1 dropdb -U synapse synapse
sudo docker exec -it matrix-postgres-1 createdb -U synapse -E UTF8 -l C -T template0 synapse


Synapse

For the Synapse container, create its data directory:

sudo mkdir /var/opt/matrix/synapse

Create its configuration file there:

/var/opt/matrix/synapse/homeserver.yaml
server_name: "quietlife.nl"
public_baseurl: "https://quietlife.nl/"
serve_server_wellknown: true
report_stats: false
media_store_path: "/data/media_store"
signing_key_path: "/data/signing.key"
form_secret: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
macaroon_secret_key: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
registration_shared_secret: "cccccccccccccccccccccccccccccccccccccccccccccccccc"

listeners:
  - port: 8008
    tls: false
    type: http
    x_forwarded: true
    resources:
      - names: [client, federation]
        compress: false

database:
  name: psycopg2
  args:
    host: "postgres"
    port: 5432
    user: "synapse"
    password: "5yN@Ps3"
    database: "synapse"

trusted_key_servers:
  - server_name: "matrix.org"

Make sure you change server_name, public_baseurl and password under database.


For form_secret, macaroon_secret_key and registration_shared_secret you have to generate random password strings.
For example:

apg -m50 -n3


If you want to add e-mail support to Synapse, append this to homeserver.yaml:

/var/opt/matrix/synapse/homeserver.yaml
email:
  smtp_host: "quietlife.nl"
  smtp_port: 587
  smtp_user: "synapse@quietlife.nl"
  smtp_pass: "PASSWORD"
  require_transport_security: true
  enable_notifs: true
  notif_from: "Quiet Chat <synapse@quietlife.nl>"
  app_name: "Quiet Chat"
  client_base_url: "https://chat.quietlife.nl"

You will, of course, have to change the majority of fields to match your own mail setup and whether or not you also host Element.


If you run your own TURN server, append this to homeserver.yaml:

/var/opt/matrix/synapse/homeserver.yaml
turn_uris: ["turn:turn.quietlife.nl?transport=udp", "turn:turn.quietlife.nl?transport=tcp"]
turn_shared_secret: "dddddddddddddddddddddddddddddddddddddddddddddddddd"
turn_user_lifetime: 86400000
turn_allow_guests: true

Make sure you change turn_uris and turn_shared_secret.


Set the permissions to 991:991:

sudo chown -R 991:991 /var/opt/matrix/synapse

And start Synapse:

cd /var/opt/matrix
sudo docker compose up -d synapse


Element

(Feel free to skip this part if you do not want to host Element. You can still use the Element desktop application or other Matrix clients if you don't.)


For the Element container, create its data directory:

sudo mkdir /var/opt/matrix/element

Create its configuration file there:

/var/opt/matrix/element/config.json
{
  "default_server_name": "quietlife.nl",
  "disable_custom_urls": true,
  "disable_3pid_login": true,
  "disable_guests": true,
  "brand": "Quiet Chat",
  "branding": {
    "auth_footer_links": [],
    "welcome_background_url": "https://quietlife.nl/resources/images/background.jpg"
  },
  "embedded_pages": {
    "login_for_welcome": true
  },
  "setting_defaults": {
    "UIFeature.registration": false
  }
}

And start Element:

cd /var/opt/matrix
sudo docker compose up -d element


nginx

A reverse proxy will be needed to do the TLS termination and to forward incoming traffic to the Docker containers.

This guide assumes that you use nginx with certificates generated by certbot.


Homeserver domain

For your homeserver domain, it's important to do two things:

There is no problem if you host another site on your root domain, as long as these subpaths can be used for Matrix.

All you need is this:

/etc/nginx/sites-available/quietlife.nl.conf
	location ~ ^/.well-known/matrix/(client|server) {
	 include proxy_params;
	 proxy_http_version 1.1;
	 proxy_pass http://localhost:8008;
	}
 
	location ~ ^(/_matrix|/_synapse/client) {
	 client_max_body_size 50M;
	 include proxy_params;
	 proxy_http_version 1.1;
	 proxy_pass http://localhost:8008;
	}


Element domain

If you want to host Element, you'll most likely want that on a subdomain, such as chat.quietlife.nl.
Of course, skip this step if you don't run the Element container.

/etc/nginx/sites-available/chat.quietlife.nl.conf
server {
	listen 80;
	listen [::]:80;
 
	listen 443 ssl;
	listen [::]:443 ssl;
	http2 on;
 
	server_name chat.quietlife.nl;
 
	access_log /var/log/nginx/chat.quietlife.nl-access.log;
	error_log /var/log/nginx/chat.quietlife.nl-error.log;
 
	ssl_certificate /etc/letsencrypt/live/chat.quietlife.nl/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/chat.quietlife.nl/privkey.pem;
 
	if ($scheme != "https") {
	 return 301 https://$host$request_uri;
	}
 
	location / {
	 add_header X-Content-Type-Options nosniff;
	 add_header X-Frame-Options SAMEORIGIN;
	 add_header X-XSS-Protection "1; mode=block";
	 include proxy_params;
	 proxy_pass http://localhost:8088;
	}
}


Database backups

It's probably a good idea to make regular backups of the Synapse database. You can use this script:

/usr/local/bin/backup-synapse.sh
#!/bin/bash
 
backup_directory="/var/backups/synapse_backups"
timestamp=$(date +"%Y-%m-%d_%H:%M")
 
# Create backup directory if not present
if [ ! -d $backup_directory ]; then
  mkdir -p $backup_directory;
fi
 
# Create a database dump and compress it
nice -n 19 docker exec matrix-postgres-1 pg_dump postgres://synapse@localhost:5432/synapse | nice -n 19 xz -T1 -1 > $backup_directory/synapse-postgres-$timestamp.sql.xz
 
# Delete backups older than one week
find $backup_directory -mtime +7 -exec rm {} \;
 
exit 0

Make it executable:

sudo chmod +x /usr/local/bin/backup-synapse.sh

And run it as a cronjob for the root user:

30 * * * * /usr/local/bin/backup-synapse.sh


Automatic updates

Because Synapse and Element run in Docker, you can't update them with unattended-upgrades.
You can use Watchtower to automate container updates, with this line in root's crontab:

0 21 * * * /usr/bin/docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once --cleanup


Creating user accounts

The quickest way to create user accounts is to talk to Synapse directly on http://localhost:8008.

Within the container, there is a register_new_matrix_user command:

docker exec -it matrix-synapse-1 register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008