Matrix Server Setup Guide

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:

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: Replace matrix.example.com with example.com in configs, but keep synapse.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). Add traefik.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. Add traefik.docker.network if 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.

Great guide for synapse matrix setup. Wonder, is this a bear metal setup or a setup on the same server on which pangolin stack is already setup and running. From the setup it seems that all is going inside a separate docker on same server of pangolin. Meaning by an additional service other than pangolin. Kindly correct me.

Yep, pretty confused about the pangolin tag as there’s no mention of pangolin anywhere in this guide.

1 Like

it overlaps pangolin traefik. you can merge with pangolin but UI will not have matrix resources.

you are absolutely spot on.

can you explain please. What you mean by this

The Matrix Ressources are still handled by Traefik, but not by Pangolins Management.
So you won’t be able to manage the ressources via the Pangolin Webinterface, yet they’ll be reachable and handled through normal Traefik reverse proxy rules that you setup according to HHF’s documentation above.

1 Like