No description
Find a file
2026-05-15 14:51:50 +02:00
templates first commit 2026-05-15 14:51:50 +02:00
.env.example first commit 2026-05-15 14:51:50 +02:00
.gitignore first commit 2026-05-15 14:51:50 +02:00
backup.sh first commit 2026-05-15 14:51:50 +02:00
docker-compose.yml first commit 2026-05-15 14:51:50 +02:00
install.sh first commit 2026-05-15 14:51:50 +02:00
LICENSE first commit 2026-05-15 14:51:50 +02:00
README.md first commit 2026-05-15 14:51:50 +02:00
setup.sh first commit 2026-05-15 14:51:50 +02:00
update.sh first commit 2026-05-15 14:51:50 +02:00

Matrix Stack

A production-ready, self-hosted Matrix homeserver stack managed via a single .env file.

Includes: Synapse · PostgreSQL 16 · Element Web · Coturn (TURN/STUN) · Traefik integration · Well-known delegation


Table of Contents


Overview

This stack deploys a fully functional Matrix homeserver using Docker Compose. All service configuration is driven by a single .env file — no need to manually edit YAML files. Config files are generated from templates at install time.

.env  ──▶  install.sh  ──▶  homeserver.yaml
                       ──▶  turnserver.conf
                       ──▶  element/config.json
                       ──▶  docker-compose up -d

Included Services

Container Image Purpose
matrix-synapse matrixdotorg/synapse:latest Matrix homeserver
matrix-postgres postgres:16-alpine Synapse database
matrix-element vectorim/element-web:latest Web client
matrix-coturn coturn/coturn:latest TURN/STUN for VoIP/video
matrix-well-known nginx:alpine Federation delegation (optional)

Requirements

Software

Requirement Version Notes
Docker Engine ≥ 24
Docker Compose plugin ≥ 2.20 docker compose version
envsubst any Part of gettext-base
curl any For health checks

Install on Debian/Ubuntu:

sudo apt install docker.io docker-compose-plugin gettext-base curl
sudo usermod -aG docker $USER   # log out and back in

Reverse Proxy

This stack is designed to work with Traefik as the reverse proxy (v2 or v3). Traefik must:

  • Have an external Docker network (default name: traefik)
  • Have a configured Let's Encrypt cert resolver (default name: letsencrypt)
  • Forward ports 80 and 443 from your router/firewall

No Traefik? See Using a Different Reverse Proxy.

DNS Records

You need DNS A records (or CNAME to your server) for:

Record Points to Purpose
matrix.example.com Your server IP Matrix homeserver
chat.example.com Your server IP Element Web client
example.com Your server IP Well-known delegation (if using a separate server name)

Quick Start

# 1. Clone the repository
git clone https://your-forgejo.example.com/jonny/matrix-stack.git
cd matrix-stack

# 2. Create your .env from the template
cp .env.example .env

# 3. Fill in all values — especially secrets and domains
nano .env

# 4. Generate secrets for all SECRET fields:
#    (run this command 5 times, once per secret)
openssl rand -hex 32

# 5. Run the installer
./install.sh

# 6. Create your first admin user
docker exec -it matrix-synapse register_new_matrix_user \
  -c /data/homeserver.yaml \
  -u admin -p 'YourSecurePassword' --admin \
  https://matrix.example.com

That's it. Open https://chat.example.com and sign in.


Configuration Reference

All configuration lives in .env. Never commit this file.

Domains

Variable Example Description
SYNAPSE_SERVER_NAME example.com Server name in MXIDs (@user:example.com)
MATRIX_DOMAIN matrix.example.com Homeserver public URL
ELEMENT_DOMAIN chat.example.com Element Web URL

Tip: If you want simple MXIDs like @alice:example.com, set SYNAPSE_SERVER_NAME=example.com and MATRIX_DOMAIN=matrix.example.com. This requires well-known delegation (see below).

For the simplest setup with no delegation needed: set both to the same value, e.g., SYNAPSE_SERVER_NAME=matrix.example.com.

Secrets

Generate each with openssl rand -hex 32:

Variable Description
POSTGRES_PASSWORD PostgreSQL password
SYNAPSE_REGISTRATION_SHARED_SECRET Used with register_new_matrix_user
SYNAPSE_MACAROON_SECRET_KEY Synapse session tokens
SYNAPSE_FORM_SECRET Synapse form CSRF protection
COTURN_AUTH_SECRET Shared between Synapse and Coturn

TURN / Coturn

Variable Default Description
COTURN_REALM Usually same as SYNAPSE_SERVER_NAME
COTURN_EXTERNAL_IP Your server's public IP
COTURN_PORT 3478 STUN/TURN port (UDP + TCP)
COTURN_TLS_PORT 5349 TURNS port
COTURN_MIN_PORT 49152 Start of UDP relay range
COTURN_MAX_PORT 49172 End of UDP relay range (21 ports = ~10 calls)

Registration

Variable Default Description
ENABLE_REGISTRATION false Allow new account creation
REGISTRATION_REQUIRES_TOKEN true Require invite token when registration is on

Performance Tuning

Variable Default Notes
SYNAPSE_MEMORY_LIMIT 1g Reduce to 512m on low-RAM hosts
POSTGRES_MEMORY_LIMIT 512m
LOG_LEVEL WARNING DEBUG / INFO for troubleshooting

Architecture

Internet
    │
    ▼ :80, :443
  Traefik (TLS termination, Let's Encrypt)
    │
    ├──▶ matrix.example.com ──▶ matrix-synapse:8008 ──▶ matrix-postgres:5432
    │                                                         (matrix_internal)
    └──▶ chat.example.com   ──▶ matrix-element:80

  Coturn (host network, direct UDP)
    :3478 UDP/TCP  ──▶ STUN/TURN
    :5349 UDP/TCP  ──▶ TURNS
    :49152-49172 UDP ──▶ media relay

Networks

Network Type Members
matrix_internal bridge, internal synapse, postgres
traefik external synapse, element, well-known
host host network coturn

PostgreSQL is not reachable from outside the matrix_internal network.


Federation & Well-Known Delegation

Matrix federation lets users on your server communicate with users on other servers (e.g., matrix.org).

Simple setup (no delegation)

Set SYNAPSE_SERVER_NAME=matrix.example.com (same as MATRIX_DOMAIN). Federation works out of the box. MXIDs will look like @alice:matrix.example.com.

Delegation setup (nice MXIDs)

For @alice:example.com with Matrix running at matrix.example.com:

  1. Set SYNAPSE_SERVER_NAME=example.com and MATRIX_DOMAIN=matrix.example.com in .env

  2. Run the installer with the delegation profile:

    ./install.sh --with-delegation
    
  3. This starts an nginx container that serves:

    • https://example.com/.well-known/matrix/server{"m.server": "matrix.example.com:443"}
    • https://example.com/.well-known/matrix/client → client config

    Note: If example.com already has a web server, add the .well-known files there instead. See ${DATA_BASE_PATH}/well-known/.well-known/matrix/ for the generated files.

  4. Verify federation:

    https://federationtester.matrix.org/#example.com
    

First Admin User

After installation, create your first admin account:

docker exec -it matrix-synapse register_new_matrix_user \
  -c /data/homeserver.yaml \
  -u yourusername \
  -p 'YourSecurePassword' \
  --admin \
  https://matrix.example.com

No admin? If you lose admin access, you can grant it via psql:

docker exec -it matrix-postgres psql -U synapse synapse \
  -c "UPDATE users SET admin=1 WHERE name='@yourusername:example.com';"

Registration Tokens

If ENABLE_REGISTRATION=true and REGISTRATION_REQUIRES_TOKEN=true, create tokens via the admin API:

curl -X POST "https://matrix.example.com/_matrix/client/v1/admin/registration_tokens/new" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"uses_allowed": 1}'

TURN Server (VoIP/Video)

Coturn runs with network_mode: host for reliable UDP relay. Required ports must be open in your firewall and (if applicable) forwarded by your router:

Port Protocol Purpose
3478 UDP + TCP STUN/TURN
5349 UDP + TCP TURNS (TLS)
4915249172 UDP Media relay

Verify TURN is working

  1. Open: https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
  2. Add a TURN server:
    • URI: turn:your-coturn-realm:3478
    • Username/Password: obtain from Synapse (call /_matrix/client/v3/voip/turnServer)
  3. Click Gather candidates — you should see relay type candidates

Conflict with existing Coturn

If you already run Coturn (e.g., for Nextcloud), you have two options:

Option A: Reuse the existing Coturn instance. Remove the coturn service from docker-compose.yml and add your existing Coturn's secret to .env as COTURN_AUTH_SECRET.

Option B: Run both on different ports. Change COTURN_PORT and COTURN_TLS_PORT to unused ports in .env.


Email Notifications

Email is optional. Leave SMTP_HOST empty in .env to disable.

When configured, Synapse sends:

  • Email verification for new accounts
  • Password reset emails
  • Notification emails (configurable per-user)

Example with a relay / SMTP provider:

SMTP_HOST=smtp.mailgun.org
SMTP_PORT=587
SMTP_USER=postmaster@mg.example.com
SMTP_PASSWORD=your-api-key
SMTP_FROM=matrix@example.com
SMTP_REQUIRE_TRANSPORT_SECURITY=true

Prometheus Metrics

Synapse exposes metrics on port 9092 (internal network only).

Prometheus scrape config

Add to your prometheus.yml:

scrape_configs:
  - job_name: 'synapse'
    static_configs:
      - targets: ['matrix-synapse:9092']
    metrics_path: '/_synapse/metrics'

If Prometheus runs in a separate Docker stack, add matrix-synapse to a shared network or use the container IP.

Grafana Dashboard

Import the official Synapse dashboard from Grafana.com:

  • Dashboard ID: 10046 (Synapse monitoring)

User Management

Common admin tasks via the Synapse Admin API:

# Set base URL and your admin token
MATRIX_URL="https://matrix.example.com"
TOKEN="your_access_token"   # from Element: Settings → Help & About → Access Token

# List all users
curl -H "Authorization: Bearer $TOKEN" \
  "$MATRIX_URL/_synapse/admin/v2/users?from=0&limit=20"

# Deactivate a user
curl -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"deactivated": true}' \
  "$MATRIX_URL/_synapse/admin/v2/users/@user:example.com"

# Reset a user's password
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"new_password": "newpassword", "logout_devices": true}' \
  "$MATRIX_URL/_synapse/admin/v1/reset_password/@user:example.com"

Maintenance

Update

Pull latest images and recreate containers:

./update.sh

This runs docker compose pull + up -d --remove-orphans + image prune.

Note: Synapse upgrades are usually smooth. If a release requires a DB migration, Synapse runs it automatically on startup. Check logs after update: docker compose logs -f synapse

Backup & Restore

Create a backup (PostgreSQL dump + all configs):

./backup.sh

Backups are stored in ${DATA_BASE_PATH}/backups/. Last 7 backups are kept.

Restore from backup:

./backup.sh --restore

What's backed up:

Content Method
PostgreSQL database pg_dump → gzip
Synapse config + signing key cp
Element config cp
Coturn config cp

What's NOT backed up by this script:

  • Media files (SYNAPSE_MEDIA_PATH) — can be large (GBs). Back these up separately with rsync or your preferred tool.

Updating configuration

After changing .env:

# Regenerate configs from templates
source .env
envsubst < templates/homeserver.yaml.template > "${DATA_BASE_PATH}/synapse/homeserver.yaml"
envsubst < templates/turnserver.conf.template > "${DATA_BASE_PATH}/coturn/turnserver.conf"
envsubst < templates/element-config.json.template > "${DATA_BASE_PATH}/element/config.json"

# Restart affected services
docker compose restart synapse coturn element

Or just re-run install.sh — it's idempotent (existing signing keys are preserved).


Firewall / Port Reference

Port Protocol Direction Service Required
80 TCP inbound Traefik (HTTP → HTTPS redirect) Yes
443 TCP inbound Traefik (Matrix + Element) Yes
3478 UDP + TCP inbound Coturn TURN/STUN For calls
5349 UDP + TCP inbound Coturn TURNS For calls
4915249172 UDP inbound Coturn media relay For calls

All other ports are internal to Docker networks and not exposed to the host or internet.


Troubleshooting

Synapse won't start

docker compose logs synapse

Common causes:

  • Database not ready: PostgreSQL health check failed. Wait 30s and try again.
  • homeserver.yaml error: Syntax error in generated config. Check LOG_LEVEL=DEBUG.
  • Signing key missing: Re-run install.sh (it regenerates the key).

Can't connect to homeserver

# Check Synapse is listening
docker compose exec synapse curl -s http://localhost:8008/_matrix/client/versions | head -c 200

# Check Traefik routing
curl -I https://matrix.example.com/_matrix/client/versions

Federation not working

# Test from your server
curl https://matrix.org/_matrix/federation/v1/version

# Test your server's federation
curl https://federationtester.matrix.org/api/report?server_name=example.com

Common causes:

  • Missing .well-known delegation (if SERVER_NAME != MATRIX_DOMAIN)
  • Port 443 not reachable from the internet
  • Invalid TLS certificate

TURN not working

# Check coturn is running and listening
ss -tulpn | grep -E '3478|5349|49152'

# Check coturn logs
docker compose logs coturn

Common causes:

  • COTURN_EXTERNAL_IP set to private IP instead of public IP
  • Relay ports not open in firewall/router
  • COTURN_AUTH_SECRET mismatch between .env and generated configs

Regenerate configs without reinstalling

# Source env and regenerate all configs
source .env
envsubst < templates/homeserver.yaml.template > "${DATA_BASE_PATH}/synapse/homeserver.yaml"
envsubst < templates/turnserver.conf.template > "${DATA_BASE_PATH}/coturn/turnserver.conf"
envsubst < templates/element-config.json.template > "${DATA_BASE_PATH}/element/config.json"
docker compose restart

Using a Different Reverse Proxy

Remove all traefik.* labels from docker-compose.yml and expose ports manually:

# In docker-compose.yml, replace labels with:
services:
  synapse:
    ports:
      - "127.0.0.1:8008:8008"
  element:
    ports:
      - "127.0.0.1:8080:80"

Then configure nginx/Caddy/Apache to proxy to those ports with TLS termination.

nginx example for Synapse:

server {
    listen 443 ssl;
    server_name matrix.example.com;

    ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8008;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $host;
        client_max_body_size 100M;
    }
}

Security Considerations

  • Never enable open registration (ENABLE_REGISTRATION=true) without also setting REGISTRATION_REQUIRES_TOKEN=true, unless you intend to run a public server.
  • Rotate secrets if they are ever exposed. After rotating, regenerate configs and restart services.
  • Media files can contain sensitive content. Ensure SYNAPSE_MEDIA_PATH has appropriate permissions (mode 750, owned by UID 991).
  • PostgreSQL is only accessible within the matrix_internal Docker network — no port is exposed to the host.
  • Coturn uses denied-peer-ip rules to prevent TURN server abuse (SSRF via relay). Do not remove these.
  • Signing key (*.signing.key) must be kept secret and backed up. Losing it breaks federation permanently for your server name.

License

MIT License — see LICENSE.


Built for self-hosters. Designed to run on a single machine.