Matrix Server Setup Guide
Matrix.org chat is split into two main parts: the server and the client. The server used in this guide is Synapse, and the client is Element Web (formerly Riot.im). Synapse requires a PostgreSQL database and optionally Redis for caching.
0. Folder Structure
Ensure your folder structure matches the following:
example/
data/
postgres/
data/
(empty)
traefik/
(empty)
matrix/
nginx/
(empty)
synapse/
(empty)
element/
(empty)
docker-compose.yml
Create them with these commands:
mkdir -p data/postgres/data
mkdir -p data/traefik
mkdir -p data/matrix/{nginx,synapse,element}
touch docker-compose.yml
1. Traefik
Create a basic Traefik configuration file. Replace your_name@example.com with your email in the ACME section for Let’s Encrypt certificates.
# data/traefik/traefik.yml
entryPoints:
web:
address: ":80"
web-secure:
address: ":443"
api:
dashboard: true
insecure: true
providers:
file:
directory: "/config"
watch: true
docker:
endpoint: "unix:///var/run/docker.sock"
network: "example_default"
watch: true
exposedByDefault: false
certificatesResolvers:
letsencrypt:
acme:
email: "your_name@example.com"
storage: "/acme.json"
httpChallenge:
entryPoint: "web"
Optional: Add HTTP to HTTPS redirection with these files (not required but recommended).
# data/traefik/config/routers.yml
http:
routers:
redirecttohttps:
entryPoints:
- "web"
middlewares:
- "httpsredirect"
rule: "HostRegexp(`{host:.+}`)"
service: "noop@internal"
# data/traefik/config/middlewares.yml
http:
middlewares:
httpsredirect:
redirectScheme:
scheme: https
permanent: true
Create an empty acme.json file for Let’s Encrypt certificates:
# data/traefik/acme.json
{}
Add the Traefik service to your docker-compose.yml:
services:
traefik:
image: "traefik:latest"
restart: "unless-stopped"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./data/traefik/traefik.yml:/etc/traefik/traefik.yml:ro"
- "./data/traefik/config:/config:ro"
- "./data/traefik/acme.json:/acme.json"
# Optional: Enable Traefik dashboard at https://traefik.example.com/
labels:
- "traefik.enable=true"
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
- "traefik.http.routers.traefik.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.traefik.entrypoints=web-secure"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
2. PostgreSQL
Set up a PostgreSQL database for Synapse.
Add to docker-compose.yml:
services:
# ... (traefik above)
postgres:
image: "postgres:16" # Updated to a recent version
restart: "unless-stopped"
environment:
POSTGRES_PASSWORD: "admin"
volumes:
- "./data/postgres/data:/var/lib/postgresql/data"
Create the Synapse database and user (ensure UTF-8 encoding):
CREATE ROLE synapse;
ALTER ROLE synapse WITH PASSWORD 'password';
ALTER ROLE synapse WITH LOGIN;
CREATE DATABASE synapse ENCODING 'UTF8' LC_COLLATE='C' LC_CTYPE='C' template=template0 OWNER synapse;
GRANT ALL PRIVILEGES ON DATABASE synapse TO synapse;
3. Redis (Optional for Caching)
Redis is used for caching and can improve performance. It’s required for worker setups but optional for basic ones.
Add to docker-compose.yml:
services:
# ... (previous services)
redis:
image: "redis:latest"
restart: "unless-stopped"
4. Synapse
Note: You’ll use two subdomains: matrix.example.com (handled by Nginx for federation) and synapse.example.com (direct to Synapse).
Generate the initial Synapse config (this creates signing keys and secrets):
docker run -it --rm \
-v $(pwd)/data/matrix/synapse:/data \
-e SYNAPSE_SERVER_NAME=matrix.example.com \
-e SYNAPSE_REPORT_STATS=yes \
-e UID=1000 \
-e GID=1000 \
element/synapse:latest generate # Updated image
This generates files like homeserver.yaml, matrix.example.com.log.config, and matrix.example.com.signing.key.
Edit data/matrix/synapse/homeserver.yaml (enable registration if desired):
server_name: "matrix.example.com"
pid_file: /data/homeserver.pid
web_client_location: https://element.example.com/ # Updated to Element
public_baseurl: https://synapse.example.com/
report_stats: true
enable_registration: true
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
resources:
- names: [client, federation]
compress: false
retention:
enabled: true
federation_ip_range_blacklist:
- '127.0.0.0/8'
- '10.0.0.0/8'
- '172.16.0.0/12'
- '192.168.0.0/16'
- '100.64.0.0/10'
- '169.254.0.0/16'
- '::1/128'
- 'fe80::/64'
- 'fc00::/7'
database:
name: psycopg2
args:
user: synapse
password: password
database: synapse
host: postgres
cp_min: 5
cp_max: 10
log_config: "/data/matrix.example.com.log.config"
media_store_path: "/data/media_store"
registration_shared_secret: "abc" # Generate a strong secret
macaroon_secret_key: "abc" # Generate a strong secret
form_secret: "abc" # Generate a strong secret
signing_key_path: "/data/matrix.example.com.signing.key"
trusted_key_servers:
- server_name: "matrix.org"
redis:
enabled: true
host: redis
port: 6379
Add to docker-compose.yml:
services:
# ... (previous services)
synapse:
image: "element/synapse:latest" # Updated image
restart: "unless-stopped"
environment:
SYNAPSE_CONFIG_DIR: "/data"
SYNAPSE_CONFIG_PATH: "/data/homeserver.yaml"
UID: "1000"
GID: "1000"
TZ: "Europe/London"
volumes:
- "./data/matrix/synapse:/data"
labels:
- "traefik.enable=true"
- "traefik.http.services.synapse.loadbalancer.server.port=8008"
- "traefik.http.routers.synapse.rule=Host(`synapse.example.com`)"
- "traefik.http.routers.synapse.entrypoints=web-secure"
- "traefik.http.routers.synapse.tls.certresolver=letsencrypt"
Test: Visit https://synapse.example.com/_matrix/static/ – it should show “It works! Synapse is running.”
5. Nginx (for Federation)
Synapse needs proper federation setup. See federation docs.
Create data/matrix/nginx/matrix.conf:
server {
listen 80 default_server;
server_name matrix.example.com;
# Traefik -> nginx -> synapse
location /_matrix/ {
proxy_pass http://synapse:8008;
proxy_set_header X-Forwarded-For $remote_addr;
client_max_body_size 128m;
}
location /.well-known/matrix/ {
root /var/www/;
default_type application/json;
add_header Access-Control-Allow-Origin *;
}
}
Create data/matrix/nginx/www/.well-known/matrix/client:
{
"m.homeserver": {
"base_url": "https://matrix.example.com"
}
}
Create data/matrix/nginx/www/.well-known/matrix/server:
{
"m.server": "synapse.example.com:443"
}
Add to docker-compose.yml:
services:
# ... (previous services)
nginx:
image: "nginx:latest"
restart: "unless-stopped"
volumes:
- "./data/matrix/nginx/matrix.conf:/etc/nginx/conf.d/matrix.conf"
- "./data/matrix/nginx/www:/var/www/"
labels:
- "traefik.enable=true"
- "traefik.http.services.matrix.loadbalancer.server.port=80"
- "traefik.http.routers.matrix.rule=Host(`matrix.example.com`)"
- "traefik.http.routers.matrix.entrypoints=web-secure"
- "traefik.http.routers.matrix.tls.certresolver=letsencrypt"
Test:
- https://matrix.example.com/.well-known/matrix/client should return the JSON above.
- https://matrix.example.com/.well-known/matrix/server should return the JSON above.
- https://matrix.example.com/_matrix/static/ should match https://synapse.example.com/_matrix/static/.
6. Federation Tester
Test at https://federationtester.matrix.org/ with matrix.example.com (no https). All checks should be green.
7. Element Web UI
Create data/matrix/element/config.json (update as needed):
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.example.com",
"server_name": "matrix.example.com"
},
"m.identity_server": {
"base_url": "https://vector.im"
}
},
"disable_custom_urls": false,
"disable_guests": false,
"disable_login_language_selector": false,
"disable_3pid_login": false,
"brand": "Element",
"integrations_ui_url": "https://scalar.vector.im/",
"integrations_rest_url": "https://scalar.vector.im/api",
"integrations_widgets_urls": [
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
],
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"defaultCountryCode": "GB",
"showLabsSettings": false,
"features": {
"feature_new_spinner": "labs",
"feature_pinning": "labs",
"feature_custom_status": "labs",
"feature_custom_tags": "labs",
"feature_state_counters": "labs"
},
"default_federate": true,
"default_theme": "light",
"roomDirectory": {
"servers": [
"matrix.org"
]
},
"welcomeUserId": "@riot-bot:matrix.org",
"piwik": {
"url": "https://piwik.riot.im/",
"whitelistedHSUrls": [
"https://matrix.org"
],
"whitelistedISUrls": [
"https://vector.im",
"https://matrix.org"
],
"siteId": 1
},
"enable_presence_by_hs_url": {
"https://matrix.org": false,
"https://matrix-client.matrix.org": false
},
"settingDefaults": {
"breadcrumbs": true
},
"jitsi": {
"preferredDomain": "jitsi.riot.im"
}
}
Add to docker-compose.yml:
services:
# ... (previous services)
element:
image: "element/element-web:latest" # Updated image
volumes:
- "./data/matrix/element/config.json:/app/config.json:ro"
labels:
- "traefik.enable=true"
- "traefik.http.services.element.loadbalancer.server.port=80"
- "traefik.http.routers.element.rule=Host(`element.example.com`)"
- "traefik.http.routers.element.entrypoints=web-secure"
- "traefik.http.routers.element.tls.certresolver=letsencrypt"
8. Logging In
Visit https://element.example.com/. Register (select “Advanced” and use https://matrix.example.com as the homeserver URL, not synapse.example.com).
9. Testing Federation
Join a public room: https://element.example.com/#/room/#hello-matrix:matrix.org.
If you see 401 errors in Synapse logs, check the SRV DNS record below.
10. SRV DNS Record (Optional)
This may be required for federation. Add an SRV record:
_matrix._tcp.matrix.example.com → 1 10 443 synapse.example.com
Test with:
dig -t SRV _matrix._tcp.matrix.example.com @8.8.8.8
Optional: OpenLDAP Integration
Integrate Synapse with OpenLDAP for authentication (disables direct registration).
Add to homeserver.yaml:
password_providers:
- module: "ldap_auth_provider.LdapAuthProvider"
config:
enabled: true
uri: "ldap://openldap:389"
start_tls: false
base: "ou=users,dc=example,dc=com"
attributes:
uid: "uid"
mail: "email"
name: "cn"
bind_dn: cn=admin,dc=example,dc=com
bind_password: password
filter: "(memberOf=cn=matrix,ou=groups,dc=example,dc=com)"
Sample OpenLDAP service in docker-compose.yml:
services:
# ... (previous services)
openldap:
image: "osixia/openldap:latest"
restart: "unless-stopped"
environment:
LDAP_ORGANISATION: "Homelab"
LDAP_DOMAIN: "dc=example,dc=com"
LDAP_ADMIN_PASSWORD: "password"
LDAP_REMOVE_CONFIG_AFTER_SETUP: "false"
volumes:
- "./data/ldap/data:/var/lib/ldap"
- "./data/ldap/config:/etc/ldap/slapd.d"
Optional: TURN Server (for VoIP/Video Calls)
For voice/video calls, set up a TURN server.
Create data/matrix/coturn/turnserver.conf:
use-auth-secret
static-auth-secret=SomeSecretPasswordForMatrix # Generate with openssl rand -hex 32
realm=matrix.example.com
listening-port=3478
tls-listening-port=5349
min-port=49160
max-port=49200
verbose
allow-loopback-peers
cli-password=SomePasswordForCLI # Generate separately
external-ip=192.168.0.2/123.123.123.123 # Local IP / Public IP
Add to homeserver.yaml:
turn_uris:
- "turn:matrix.example.com:3478?transport=udp"
- "turn:matrix.example.com:3478?transport=tcp"
- "turns:matrix.example.com:3478?transport=udp"
- "turns:matrix.example.com:3478?transport=tcp"
turn_shared_secret: "SomeSecretPasswordForMatrix"
turn_user_lifetime: 86400000
turn_allow_guests: True
Add to docker-compose.yml:
services:
# ... (previous services)
coturn:
image: "instrumentisto/coturn:latest"
restart: "unless-stopped"
volumes:
- "./data/matrix/coturn/turnserver.conf:/etc/coturn/turnserver.conf"
ports:
- "49160-49200:49160-49200/udp"
- "3478:3478"
- "5349:5349"
Open firewall ports (e.g., with UFW):
sudo ufw allow 5349
sudo ufw allow 3478
sudo ufw allow 49160:49200/udp
Forward these ports on your router: 3478 (TCP/UDP), 5349 (TCP/UDP), 49160-49200 (UDP).
Restart Synapse after changes: docker restart <synapse-container-name>.
Test calls on separate networks to verify.
Note: If using VPN, adjust external-ip to VPN IPs and keep allow-loopback-peers.
Final Folder Structure
Your structure should resemble:
example/
data/
postgres/
data/
... (populated)
traefik/
config/
middlewares.yml
routers.yml
traefik.yml
acme.json
matrix/
coturn/ (optional)
turnserver.conf
nginx/
www/
.well-known/
matrix/
server
client
matrix.conf
synapse/
media_store/
... (populated)
homeserver.yaml
matrix.example.com.log.config
matrix.example.com.signing.key
element/
config.json
docker-compose.yml
FAQ (Based on User Questions)
Here are common questions and answers from users who followed this guide:
-
Q: How do I set it up so my user is @user:example.com instead of @user:matrix.example.com? (I still want to use the base domain for other sites.)
A: Replacematrix.example.comwithexample.comin configs, but keepsynapse.example.com. For the base domain, use Traefik rules like:- "traefik.http.routers.matrix.rule=Host(example.com) && PathPrefix(/_matrix)". This routes /_matrix/* to Nginx/Synapse while allowing other paths for your sites. You may need /.well-known/matrix/ too:- "traefik.http.routers.matrix.rule=(Host($DOMAIN) && PathPrefix(/.well-known/matrix/,/_matrix/))". The .well-known files are for federation discovery; SRV records are optional but recommended. -
Q: I’m getting constant timeouts to matrix.mydomain.com, but synapse.mydomain.com works.
A: Check Nginx/Synapse communication (ports, network). Verify Traefik labels and dashboard (service/router should appear). Addtraefik.docker.network=<network>to containers. Test internally with curl from another container:curl -v -H "Host: matrix.mydomain.com" http://<nginx-ip>/_matrix/. -
Q: Can I do without the Nginx container?
A: Nginx serves static .well-known files for federation. Traefik can’t serve them directly, so a web server like Nginx is needed. If no federation, you might skip it. -
Q: Registration breaks on sending email (CORS error, 504 timeout).
A: Ensure domains match configs (don’t change after generating). Check CORS headers from Synapse. Addtraefik.docker.networkif needed. The synapse subdomain is for SRV records and direct access. -
Q: Does the integration server work (e.g., for stickers)?
A: If errors occur, verify Element config (integrations URLs). Test federation fully. -
Q: Federation tester fails on port 8448.
A: This might be a default fallback. Ensure your SRV record and ports (80/443) are correct. Use the right FQDN. -
Q: Can I use this without Traefik (e.g., plain Nginx reverse proxy)?
A: Yes, adapt the proxy rules to your Nginx setup. See forks or official docs for alternatives. -
Q: Traefik uses default cert instead of Let’s Encrypt.
A: Check logs for resolver errors (e.g., “non-existent resolver: letsencrypt”). Ensure ACME config is correct. For 404 paths, default cert is normal. -
Q: Is Redis still required?
A: Not for basic setups in recent versions, but included here for caching. Official docker-compose doesn’t include it by default—remove if unneeded. -
Q: Can Coturn be configured with Traefik labels?
A: Coturn uses UDP/TCP ports directly; Traefik handles HTTP/HTTPS. Expose ports as shown, no labels needed for Coturn. -
Q: Do I need a public domain for internal federation?
A: No, but use internal DNS records for your servers.