How to Self-Host Matrix Synapse: Decentralized E2E Encrypted Messaging 2026
TL;DR
Matrix is an open standard for decentralized, E2E-encrypted messaging. Synapse (Apache 2.0, ~12K GitHub stars, Python) is the reference homeserver — your own private messaging infrastructure that federates with the global Matrix network. Element is the polished web/desktop/mobile client. Matrix is the chat infrastructure used by the German federal government, France's Defense Ministry, and thousands of companies who need encrypted, self-sovereign communications.
Key Takeaways
- Matrix/Synapse: Apache 2.0, ~12K stars — decentralized, federated, E2E encrypted messaging
- Federation: Users on your server can message users on any Matrix server (matrix.org, etc.)
- Bridges: Connect your Matrix server to Slack, Discord, Telegram, WhatsApp, Signal via bridges
- Element: The best Matrix client — web, desktop, iOS, Android — all free
- E2E by default: All DMs and rooms can be E2E encrypted; keys verified via cross-signing
- vs Signal: Matrix is self-hosted and federated; Signal is centralized but simpler
Part 1: Docker Setup
# docker-compose.yml
services:
synapse:
image: matrixdotorg/synapse:latest
container_name: synapse
restart: unless-stopped
ports:
- "8008:8008"
volumes:
- synapse_data:/data
environment:
SYNAPSE_SERVER_NAME: "yourdomain.com"
SYNAPSE_REPORT_STATS: "no"
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: synapse
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
POSTGRES_DB: synapse
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U synapse"]
interval: 10s
start_period: 20s
volumes:
synapse_data:
postgres_data:
Generate Synapse config
# Generate the homeserver.yaml config:
docker run --rm \
-v synapse_data:/data \
-e SYNAPSE_SERVER_NAME=yourdomain.com \
-e SYNAPSE_REPORT_STATS=no \
matrixdotorg/synapse:latest generate
# The config is now at: synapse_data/homeserver.yaml
# Edit it to configure PostgreSQL:
# homeserver.yaml (key sections):
server_name: "yourdomain.com"
# PostgreSQL database:
database:
name: psycopg2
args:
user: synapse
password: "your-db-password"
database: synapse
host: postgres
cp_min: 5
cp_max: 10
# Email for notifications:
email:
smtp_host: smtp.yourdomain.com
smtp_port: 587
smtp_user: "matrix@yourdomain.com"
smtp_pass: "your-smtp-password"
notif_from: "Matrix <matrix@yourdomain.com>"
enable_notifs: true
# Disable registration (invite-only):
enable_registration: false
registration_requires_token: true
# Enable federation:
federation_domain_whitelist:
# Remove to allow all federation, or list allowed servers:
# - matrix.org
# - element.io
# Start Synapse:
docker compose up -d
# Create admin user:
docker exec synapse register_new_matrix_user \
-u admin -p your-password -a \
-c /data/homeserver.yaml http://localhost:8008
Part 2: HTTPS with Caddy
Matrix requires specific routing for federation:
yourdomain.com {
# Matrix federation API:
handle /_matrix/* {
reverse_proxy localhost:8008
}
# Matrix client API:
handle /.well-known/matrix/* {
respond `{"m.homeserver":{"base_url":"https://yourdomain.com"},"m.identity_server":{"base_url":"https://vector.im"}}` 200
}
}
# If using a subdomain for Matrix (matrix.yourdomain.com):
matrix.yourdomain.com {
reverse_proxy localhost:8008
}
Federation delegation (recommended)
Run Synapse on matrix.yourdomain.com but have user IDs as @user:yourdomain.com:
yourdomain.com {
handle /.well-known/matrix/server {
respond `{"m.server":"matrix.yourdomain.com:443"}` 200
}
handle /.well-known/matrix/client {
respond `{"m.homeserver":{"base_url":"https://matrix.yourdomain.com"}}` 200
}
}
matrix.yourdomain.com {
reverse_proxy localhost:8008
}
Part 3: Element Web Client
Host Element Web yourself:
# Add to docker-compose.yml:
services:
element-web:
image: vectorim/element-web:latest
container_name: element
restart: unless-stopped
ports:
- "8080:80"
volumes:
- ./element-config.json:/app/config.json:ro
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.yourdomain.com",
"server_name": "yourdomain.com"
}
},
"disable_custom_urls": true,
"brand": "My Matrix Chat",
"show_labs_settings": false,
"features": {
"feature_video_rooms": false
}
}
chat.yourdomain.com {
reverse_proxy localhost:8080
}
Part 4: Bridges
Bridges connect your Matrix server to other chat platforms:
Telegram bridge (mautrix-telegram)
# Add to docker-compose.yml:
services:
mautrix-telegram:
image: dock.mau.dev/mautrix/telegram:latest
container_name: mautrix_telegram
restart: unless-stopped
volumes:
- ./bridges/telegram:/data
depends_on:
- synapse
# Generate config:
docker run --rm -v ./bridges/telegram:/data dock.mau.dev/mautrix/telegram:latest
# Edit ./bridges/telegram/config.yaml:
# homeserver.address: https://matrix.yourdomain.com
# bridge.permissions: {"@admin:yourdomain.com": "admin"}
# appservice.as_token and hs_token: generate random strings
# Register bridge with Synapse (add to homeserver.yaml):
# app_service_config_files:
# - /data/telegram-registration.yaml
Available bridges
| Platform | Bridge | GitHub |
|---|---|---|
| Telegram | mautrix-telegram | dock.mau.dev/mautrix/telegram |
| mautrix-whatsapp | dock.mau.dev/mautrix/whatsapp | |
| Discord | mautrix-discord | dock.mau.dev/mautrix/discord |
| Slack | mautrix-slack | dock.mau.dev/mautrix/slack |
| Signal | mautrix-signal | dock.mau.dev/mautrix/signal |
| iMessage | mautrix-imessage | dock.mau.dev/mautrix/imessage (macOS only) |
Part 5: User Management
# Create a user:
docker exec synapse register_new_matrix_user \
-u alice -p secure-password \
-c /data/homeserver.yaml http://localhost:8008
# Create a registration token (for invite-only signup):
docker exec synapse synapse_port_db --config-file /data/homeserver.yaml \
--generate-registration-token
# Or via Admin API:
curl -X POST "https://matrix.yourdomain.com/_synapse/admin/v1/registration_tokens/new" \
-H "Authorization: Bearer $ADMIN_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"uses_allowed": 1}'
# List users:
curl "https://matrix.yourdomain.com/_synapse/admin/v2/users?from=0&limit=100" \
-H "Authorization: Bearer $ADMIN_ACCESS_TOKEN" | jq '.users[].name'
# Deactivate a user:
curl -X PATCH "https://matrix.yourdomain.com/_synapse/admin/v2/users/@alice:yourdomain.com" \
-H "Authorization: Bearer $ADMIN_ACCESS_TOKEN" \
-d '{"deactivated": true}'
Part 6: E2E Encryption and Cross-Signing
Matrix supports E2E encryption with device verification:
- Enable E2E in a room: Room Settings → Security → Enable Encryption
- Cross-signing: In Element → Security → Set up → creates a cross-signing key
- Verification: Verify other users' devices via emoji comparison or QR code
- Secure backup: Store encrypted key backup so you don't lose messages if you lose your device
# Check E2E status via API:
curl "https://matrix.yourdomain.com/_matrix/client/v3/keys/query" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"device_keys": {"@alice:yourdomain.com": []}}'
Maintenance
# Update Synapse:
docker compose pull
docker compose up -d
# Database maintenance (run monthly):
docker exec synapse synapse_port_db --config-file /data/homeserver.yaml --update-stats
# Purge old room events (reduce database size):
curl -X POST "https://matrix.yourdomain.com/_synapse/admin/v1/purge_history_jobs" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"delete_local_events": true, "purge_up_to_ts": 1609459200000}'
# Backup:
docker exec synapse-postgres-1 pg_dump -U synapse synapse \
| gzip > synapse-db-$(date +%Y%m%d).sql.gz
# Logs:
docker compose logs -f synapse
See all open source communication tools at OSSAlt.com/categories/communication.