Self-Hosting Decentralized Chat: Matrix Synapse on Debian Linux (Olm/Megolm E2EE, Federation & PostgreSQL)
Executive Summary: Matrix is an open IETF/Matrix.org standard for decentralized, federated, End-to-End Encrypted (E2EE) real-time communication. Just as an email user at
@nejat-hakan.decan message anyone at@debian.orgwithout sharing a central server, a Matrix Synapse Homeserver gives you sovereign identities (@nejat:nejat-hakan.de) encrypted via the Double-Ratchet Olm/Megolm cryptographic state machine and accessible from sleek modern clients like Element X, Element Web, and FluffyChat.
-
Sovereign
@user:nejat-hakan.deID
Use
.well-known/matrix/serverdelegation so your Matrix ID uses your clean root domain (@nejat:nejat-hakan.de) while hosting Synapse onmatrix.nejat-hakan.de. -
Olm & Megolm Double-Ratchet E2EE
Cross-signed device verification ensures that private rooms and voice/video calls remain cryptographically unreadable even to intermediate federated servers.
-
Mandatory
CCollation PostgreSQL
Configure PostgreSQL 17 with
--lc-collate=C --lc-ctype=Cas required by Synapse's directed acyclic event graph (DAG) state resolver. -
Federation or Private-Only Citadel
Join global open-source developer rooms (
#debian:matrix.org) or lockfederation_domain_whitelistfor a 100% isolated internal organization server.
1. Matrix Federation & .well-known Domain Delegation Architecture
A common mistake when deploying Matrix Synapse is setting server_name: matrix.nejat-hakan.de in homeserver.yaml—which permanently locks your user IDs to @nejat:matrix.nejat-hakan.de (server_name cannot be changed after database initialization!).
Instead, set server_name: "nejat-hakan.de" and serve two tiny JSON endpoints from your root website (https://nejat-hakan.de/.well-known/matrix/server and client) that delegate Matrix traffic to matrix.nejat-hakan.de:443:
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
ELEM["📱 Element X / Web Client<br/>User: @nejat:nejat-hakan.de"]
WELL["🌐 nejat-hakan.de/.well-known/<br/>matrix/client & server<br/>(Delegates to matrix.*:443)"]
SYN["🛡️ Matrix Synapse Server<br/>matrix.nejat-hakan.de<br/>(127.0.0.1:8008)"]
PG["🗄️ PostgreSQL 17 (LC_COLLATE=C)<br/>Event DAG & State Store"]
ELEM -->|"1. Lookup .well-known"| WELL
ELEM <==>|"2. Client-Server API (:443)"| SYN
SYN <--> PG
Configuring .well-known Delegation on nejat-hakan.de
Serve these two JSON payloads with Access-Control-Allow-Origin: * on your root domain (nejat-hakan.de):
https://nejat-hakan.de/.well-known/matrix/server:{"m.server": "matrix.nejat-hakan.de:443"}https://nejat-hakan.de/.well-known/matrix/client:{"m.homeserver": {"base_url": "https://matrix.nejat-hakan.de"}}
2. Generating homeserver.yaml & Deploying with PostgreSQL (LC_COLLATE=C)
Synapse Requires LC_COLLATE=C and LC_CTYPE=C in PostgreSQL!
Matrix Synapse refuses to start if its PostgreSQL database is initialized with a locale other than C. Always pass POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" when creating the Synapse database container.
# File: /opt/matrix-ops/01_generate_synapse_config.sh
#!/usr/bin/env bash
set -euo pipefail
sudo mkdir -p /srv/containers/synapse/{data,postgres}
# Generate initial homeserver.yaml and Ed25519 signing key for 'nejat-hakan.de'
docker run -it --rm \
-v /srv/containers/synapse/data:/data \
-e SYNAPSE_SERVER_NAME=nejat-hakan.de \
-e SYNAPSE_REPORT_STATS=no \
docker.io/matrixdotorg/synapse:latest generate
Now deploy compose.yaml:
# File: /srv/containers/synapse/compose.yaml
services:
synapse:
image: docker.io/matrixdotorg/synapse:latest
container_name: matrix-synapse
depends_on:
synapse-db:
condition: service_healthy
restart: unless-stopped
ports:
- "127.0.0.1:8008:8008/tcp"
environment:
UID: "991"
GID: "991"
TZ: "Europe/Berlin"
volumes:
- /srv/containers/synapse/data:/data:rw
synapse-db:
image: docker.io/library/postgres:17-bookworm
container_name: matrix-postgres
restart: unless-stopped
environment:
POSTGRES_USER: "synapse"
POSTGRES_PASSWORD: "${SYNAPSE_DB_PASSWORD}"
POSTGRES_DB: "synapse"
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumes:
- /srv/containers/synapse/postgres:/var/lib/postgresql/data:rw
healthcheck:
test: ["CMD-SHELL", "pg_isready -U synapse -d synapse"]
interval: 10s
timeout: 5s
retries: 5
3. Essential homeserver.yaml Hardening Settings
Edit /srv/containers/synapse/data/homeserver.yaml to connect PostgreSQL, disable open public registration, and configure shared-secret registration for CLI user creation:
# File: /srv/containers/synapse/data/homeserver.yaml (Key Sections)
server_name: "nejat-hakan.de"
public_baseurl: "https://matrix.nejat-hakan.de/"
pid_file: /data/homeserver.pid
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
bind_addresses: ['0.0.0.0']
resources:
- names: [client, federation]
compress: false
database:
name: psycopg2
args:
user: synapse
password: "Your_Strong_Synapse_DB_Password"
database: synapse
host: synapse-db
cp_min: 5
cp_max: 10
enable_registration: false
registration_shared_secret: "Replace_With_32_Byte_Random_Hex_Secret"
suppress_key_server_warning: true
Create your admin user via CLI:
# File: /opt/matrix-ops/02_register_admin_user.sh
docker exec -it matrix-synapse register_new_matrix_user \
-u nejat \
-p "Strong_Matrix_Password_2026!" \
-a \
-c /data/homeserver.yaml \
http://127.0.0.1:8008
Where to Go Next
Explore Mattermost team collaboration, Caddy reverse proxying, and off-site backups:
-
Mattermost Team Chat
Compare decentralized Matrix Synapse with Mattermost's single-binary Slack-compatible DevOps chat.
-
Caddy Reverse Proxy
Serve
/.well-known/matrix/*JSON headers and reverse-proxymatrix.nejat-hakan.deeffortlessly with Caddy. -
Vaultwarden Password Manager
Back up your Element E2EE Recovery Key (
Security Key) safely inside your Vaultwarden vault.