Run your own Synapse server including Element Call and Element Web

Once again, this is mainly a post for myself, so that I do not forget everything the next time I have to set up a Matrix server. This time it is about Synapse, the reference server for the Matrix protocol.

If you have found this page, you probably already know what Synapse and the Matrix protocol are. Besides normal textchat, it can also handle voice and video calls and many other things. The Matrix protocol is very powerful. Unfortunately, this also makes it very complicated. As a result, there are still not many serious alternatives to Synapse or the Element clients if you want to use the full feature set.

So why am I spending time on this? In everyday life I mainly use Signal. In my opinion it is an excellent service, although some people will probably disagree immediately. However, it is a centralised service, it depends on a mobile phone number, it is within Donald Trump’s jurisdiction, and if the planned chat control ever becomes reality, Signal has already said that they might leave the European market.

For me, my own Synapse server is therefore a kind of emergency plan for communication. It is also very useful for smart home notifications, privacy-related applications and similar things. Matrix is designed to be decentralised, so it is a bit unfortunate that so many users have made matrix.org their permanent home.

Setting up a Synapse server, including the backend for Element Call, is unfortunately not straightforward. That is why I decided to collect everything in one place. This is not a step-by-step tutorial explaining every single line. At some point you still have to think for yourself. The guide is also based on my own setup, namely Docker with a native nginx reverse proxy. Your setup may be different, but perhaps this can still serve as a useful reference. I had to collect the required information from many different sources myself. Therefore, I will include all relevant configuration files so that you can compare them with your own.

Originally I wanted to upload the files to Codeberg, but apparently the service is currently having some problems.

At the end, you should have a working Synapse server including Element Web and Element Call.

Requirements:

  1. Internet facing Linux server with docker, docker compose and nginx. If you use a firewall dont forget to confgigure it correctly
  2. Two Domains i use matrix.example.eu and matrixrtc.example.eu, you have to replace them on any accurance
  3. TLS Certs for both domains

Folder structure

The first step is to create a suitable folder structure. In my case, every container together with its configuration files has its own directory below /opt. For my Matrix server it looks like this.

opt
└── matrix
    ├── elementweb
    ├── livekit
    ├── postgres
    ├── synapse
    └── docker-compose.yaml

You do not need to create the docker-compose.yaml file yet.

Generate homeserver.yaml

Once the directory structure is ready, let Synapse generate the initial homeserver.yaml together with all required keys and secrets. You can do this with the following command. Afterwards you will find the generated homeserver.yaml inside the synapse directory. If your directory layout is different, simply adjust the command accordingly.

docker run -it --rm \
  -v /opt/matrix/synapse:/data \
  -e SYNAPSE_SERVER_NAME=matrix.example.eu \
  -e SYNAPSE_REPORT_STATS=no \
  matrixdotorg/synapse:latest generate

Editing homeserver.yaml

Now open the generated homeserver.yaml and apply the changes shown in my example. Please do not simply copy and paste everything, otherwise your own keys and secrets will be overwritten. Go through the file line by line. If you are unsure about a setting, have a look at the Synapse documentation. As already mentioned, you still have to think for yourself from time to time. Also make sure to choose a proper password for the PostgreSQL database. You will need exactly the same password later in your Docker Compose configuration.

# Configuration file for Synapse.
#
# This is a YAML file: see [1] for a quick introduction. Note in particular
# that *indentation is important*: all the elements of a list or dictionary
# should have the same indentation.
#
# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html
#
# For more information on how to configure Synapse, including a complete accounting of
# each option, go to docs/usage/configuration/config_documentation.md or
# https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html
server_name: "matrix.example.eu"
pid_file: /data/homeserver.pid
listeners:
  - port: 8008
    resources:
    - compress: false
      names:
      - client
      - federation
    tls: false
    type: http
    x_forwarded: true
database:
  name: psycopg2
  args:
    user: synapse
    password: mySuperSecretPassword
    database: synapse
    host: db
    port: 5432
    cp_min: 5
    cp_max: 10
log_config: "/data/matrix.example.eu.log.config"
media_store_path: /data/media_store
max_upload_size: 50M
enable_registration: false
enable_registration_without_verification: false
registration_shared_secret: "AutoGenerated"
# Retention policy
retention:
  enabled: true
  default_policy:
    min_lifetime: 1d
    max_lifetime: 365d
url_preview_enabled: true
url_preview_ip_range_blacklist:
  - '127.0.0.0/8'
  - '10.0.0.0/8'
  - '172.16.0.0/12'
  - '192.168.0.0/16'
report_stats: false
macaroon_secret_key: "AutoGenerated"
form_secret: "AutoGenerated"
signing_key_path: "/data/matrix.example.eu.signing.key"
trusted_key_servers:
  - server_name: "matrix.org"
experimental_features:
  # MSC3266: Room summary API. Used for knocking over federation
  msc3266_enabled: true
  # MSC4222: needed for syncv2 state_after. This allows clients to
  # correctly track the state of the room.
  msc4222_enabled: true
  # MSC4140: Delayed events are required for proper call participation signalling. If disabled it is very likely that you end up with stuck calls in Matrix rooms
  msc4140_enabled: true

# The maximum allowed duration by which sent events can be delayed, as
# per MSC4140.
max_event_delay_duration: 24h

rc_message:
  # This needs to match at least e2ee key sharing frequency plus a bit of headroom
  # Note key sharing events are bursty
  per_second: 0.5
  burst_count: 30
  # This needs to match at least the heart-beat frequency plus a bit of headroom
  # Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
rc_delayed_event_mgmt:
  per_second: 1
  burst_count: 20

Edit docker-compose.yaml

Next, create the docker-compose.yaml file. The file is already commented, so it should be reasonably clear which values need to be changed. The most important ones are the database password as well as the key and secret for LiveKit. The comments also explain how to generate these values.

services:
  synapse:
    image: matrixdotorg/synapse:latest
    container_name: synapse
    restart: unless-stopped
    volumes:
      - ./synapse:/data
    ports:
      - "127.0.0.1:8008:8008"   # Bind to loopback
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    container_name: synapse-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: synapse
      POSTGRES_PASSWORD: CHANGEME #also in synapse/homeserver.yaml
      POSTGRES_DB: synapse
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
    volumes:
      - ./postgres:/var/lib/postgresql/data
    ports:
      - "127.0.0.1:5432:5432"

  auth-service:
    image: ghcr.io/element-hq/lk-jwt-service:latest
    container_name: element-call-jwt
    hostname: auth-server
    environment:
      - LIVEKIT_JWT_PORT=8080
      - LIVEKIT_URL=https://matrixrtc.example.eu/livekit/sfu #CHANGEME
      - LIVEKIT_KEY=CHANGEME # use tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 64 to generate key / also change in livekit/config.yaml
      - LIVEKIT_SECRET=CHANGEME # use tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 64 to generate key / also change in livekit/config.yaml
      - LIVEKIT_FULL_ACCESS_HOMESERVERS=matrix.example.eu
    restart: unless-stopped
    ports:
      - 127.0.0.1:8070:8080 #Change 8070 to whichever port you want JWT to be available on locally

  livekit:
    image: livekit/livekit-server:latest
    container_name: element-call-livekit
    command: --config /etc/livekit.yaml
    ports:
      - 127.0.0.1:7880:7880/tcp
      - 7881:7881/tcp
      - 50100-50200:50100-50200/udp
    restart: unless-stopped
    volumes:
      - ./livekit/config.yaml:/etc/livekit.yaml:ro

  element-web:
    image: vectorim/element-web:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:8009:80"
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O /dev/null http://localhost:80/version || exit 1"]
      start_period: "5s"
      interval: "15s"
      timeout: "5s"
    volumes:
      - ./element-web/config.json:/app/config.json
    depends_on:
      - synapse

Livekit configuration

The next file to edit is config.yaml inside the livekit directory. Here you mainly have to adjust the external IP address of your server and insert the LiveKit key and secret you generated before. By the way, I recently came across an interesting article (https://sspaeth.de/2026/04/matrix-voip-and-livekit/) explaining that, in most cases, you will not need a TURN server at all—neither the one built into LiveKit nor a separate CoTURN installation. One less service to maintain is rarely a bad thing.

port: 7880
bind_addresses:
  - "0.0.0.0"
rtc:
  tcp_port: 7881
  port_range_start: 50100
  port_range_end: 50200
  use_external_ip: true
  node_ip: Externe IP eures Servers
room:
  auto_create: false
logging:
  level: info
turn:
  enabled: false
  domain: localhost
  cert_file: ""
  key_file: ""
  tls_port: 5349
  udp_port: 443
  external_tls: true
keys:
  LIVEKIT_KEY: LIVEKIT_SECRET # Values from your docker compose, mind the space!

Element Web configuration

The last configuration file is the JSON configuration for Element Web, assuming you want to use it. Once again, you mainly need to adjust the URLs and a few other values so that they match your own setup.

{
    "default_server_config": {
        "m.homeserver": {
            "base_url": "https://matrix.example.eu",
            "server_name": "matrix.example.eu"
        },
        "m.identity_server": {
            "base_url": "https://vector.im"
        }
    },
    "disable_custom_urls": false,
    "disable_guests": false,
    "disable_login_language_selector": false,
    "disable_3pid_login": false,
    "force_verification": false,
    "brand": "Element",
    "default_widget_container_height": 280,
    "default_country_code": "DE",
    "show_labs_settings": false,
    "features": {
        "feature_video_rooms": true,
        "feature_group_calls": true,
        "feature_element_call_video_rooms": true,
        "feature_oidc_native_flow": true
    },
    "default_federate": true,
    "default_theme": "light",
    "room_directory": {
        "servers": ["https://matrix.example.eu"]
    },
    "setting_defaults": {
        "breadcrumbs": true
    },
    "element_call": {
        "url": "https://matrixrtc.example.eu"
    },
    "map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
}

Nginx configuration

To make your Synapse server reachable from the Internet and allow federation with other Matrix servers, you also need a suitable reverse proxy. In my case this is nginx. You can use the following configuration as a starting point. Of course, you have to replace the domain names and the paths to your TLS certificates with your own values.

# HTTPS redirect
server {
    listen 80;
    listen [::]:80;
    server_name matrix.example.eu;

    location / {
        return 301 https://$host$request_uri;
    }
}

# Client API
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name matrix.example.eu;

    ssl_certificate     /etc/letsencrypt/live/matrix.example.eu/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/matrix.example.eu/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;


    client_max_body_size 50M;

    # Well-known for Client Configuration
    location /.well-known/matrix/client {
        return 200 '{"m.homeserver": {"base_url": "https://matrix.example.eu"}, "m.identity_server": {"base_url": "https://vector.im"}, "org.matrix.msc4143.rtc_foci": [{"type": "livekit", "livekit_service_url": "https://matrixrtc.example.eu/livekit/jwt"}]}';
        default_type application/json;
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods 'GET, OPTIONS';
    }

    # Well-known for federation
    location /.well-known/matrix/server {
        return 200 '{"m.server":"matrix.example.eu:8448"}';
        default_type application/json;
    }

    location / {
        proxy_pass http://localhost:8009;
        proxy_set_header X-Forwarded-For $remote_addr;
    }

    # Forward to dockerized Synapse
    location ~* ^(\/_matrix|\/_synapse\/client) {
        proxy_pass         http://localhost:8008;
        proxy_set_header   X-Forwarded-For $remote_addr;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_set_header   Host $host;
        proxy_http_version 1.1;

        proxy_read_timeout    600;
        proxy_connect_timeout 600;
        proxy_send_timeout    600;
    }
}

# Federation Port 8448
server {
    listen 8448 ssl http2;
    listen [::]:8448 ssl http2;
    server_name matrix.example.eu;

    ssl_certificate     /etc/letsencrypt/live/matrix.example.eu/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/matrix.example.eu/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    client_max_body_size 50M;


    location / {
        proxy_pass         http://localhost:8008;
        proxy_set_header   X-Forwarded-For $remote_addr;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_set_header   Host $host;
        proxy_http_version 1.1;
    }
}
# HTTPS redirect
server {
    listen 80;
    listen [::]:80;
    server_name matrixrtc.example.eu;

    location / {
        return 301 https://$host$request_uri;
    }
}

# HTTPS Client API
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name matrixrtc.example.eu;

    ssl_certificate     /etc/letsencrypt/live/matrixrtc.example.eu/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/matrixrtc.example.eu/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

location ^~ /livekit/jwt/ {
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;

      # MatrixRTC Authorization Service running at port 8080
      proxy_pass http://localhost:8070/;
    }

    location ^~ /livekit/sfu/ {
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;

      proxy_send_timeout 120;
      proxy_read_timeout 120;
      proxy_buffering off;

      proxy_set_header Accept-Encoding gzip;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";


      # LiveKit SFU websocket connection running at port 7880
      proxy_pass http://localhost:7880/;
    }
}

After that, create the appropriate symbolic link so that nginx knows about the new configuration and reload the service. Danach konnt Ihr wieder in den Ordner /opt/matrix wechseln und den Synapse Server starten:

Start Synapse Server

Once this is done, change back to your /opt/matrix directory and start the Synapse server.

docker compose up

For the first start I deliberately left out the detached mode. This way you can watch the log output and also see when the database initialisation has finished.

Creating users

Since registration is disabled, you have to create users from the command line. The first user can also be made an administrator straight away. For any additional users, simply replace –admin with –no-admin.

docker exec -it synapse register_new_matrix_user \
  http://localhost:8008 \
  -c /data/homeserver.yaml \
  -u meinErsterUser \
  -p SuperSicheresPasswort \
  --admin

At this point your own Synapse server should be running and should also be able to federate with other Matrix servers. If you notice mistakes in the configuration or have constructive suggestions, feel free to leave a comment. Setting up a complete Matrix environment is quite complex, and there is no single guide that works perfectly for every setup (and this one isnt it either).

Still, I hope this article encourages some people to set up their own Matrix server. Every additional independent server makes the Matrix ecosystem a little more resilient, and that can only be a good thing.

Björns Techblog
Björns Techblog
@blog@blog.sengotta.net
354 Beiträge
73 Folgende
Fediverse-Reaktionen

Kommentare

  • @blog so much stuff to do. Does anyone know if this is also possible without docker? And it should be much easier, so anyone out there who can simplify this by some installation routine?

    1. yes it is complex, but i dont know if it gets easier with a bare metal install without docker. On the other hand you cann reduce complexity if you dont need Element Call or Element Web.

  • Schreibe einen Kommentar