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:
- Internet facing Linux server with docker, docker compose and nginx. If you use a firewall dont forget to confgigure it correctly
- Two Domains i use matrix.example.eu and matrixrtc.example.eu, you have to replace them on any accurance
- 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.
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?
Remote-Antwort
Ursprüngliche Kommentar-URL
Dein Profil
Warum muss ich mein Profil eingeben?
Diese Website ist Teil des ⁂ Open Social Web, einem Netzwerk miteinander verbundener sozialer Plattformen (wie beispielsweise Mastodon, Pixelfed, Friendica und andere). Im Gegensatz zu zentralisierten sozialen Medien befindet sich dein Profil auf einer Plattform deiner Wahl, und du kannst mit Menschen auf verschiedenen Plattformen interagieren.
Wenn du dein Profil eingibst, können wir an dein Profil senden, wo du diese Aktion abschließen kannst.
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.