Most people rely on WhatsApp, Telegram, or Slack for daily communication — yet each of these platforms holds your message history on servers you cannot audit, control, or take with you when you leave. The Matrix protocol solves this by offering a decentralized, end-to-end encrypted messaging standard that you can host yourself, bridge to other networks, and federate with the global Matrix network.
This guide walks you through the full self-hosted Matrix stack: the homeserver (Synapse or Dendrite), the client (Element), and the bridges that connect Matrix to WhatsApp, Signal, Telegram, IRC, and more.
Running your own messaging server gives you advantages no proprietary platform can match:
- Complete data ownership — every message, file, and metadata record lives on your hardware. No third party scans, mines, or sells your conversation patterns.
- End-to-end encryption by default — Matrix supports Olm/Megolm E2EE for private rooms. Even the server admin cannot read encrypted messages.
- Federation without lock-in — your server can communicate with any other Matrix server worldwide, while keeping your data local. You are not siloed.
- Bridging to proprietary networks — connect Matrix to WhatsApp, Signal, Telegram, Discord, IRC, and Slack through bridges. One interface for everything.
- No subscription fees — open-source, no per-user licensing, no premium tiers. The entire stack runs on a $5/month VPS.
- Compliance and auditability — for organizations handling sensitive data, self-hosting means you control retention policies, backups, and access logs.
Matrix Protocol vs Synapse vs Element: Understanding the Stack
Before deploying, it helps to understand how the pieces fit together:
| Component | Role | Analogy |
|---|
| Matrix Protocol | Open standard for decentralized communication | Like SMTP for email — defines how servers talk |
| Synapse | Reference homeserver implementation (Python) | Like Postfix — the actual server software |
| Dendrite | Second-gen homeserver (Go, monolithic or polylithic) | Like Exim — lighter alternative server |
| Conduit | Lightweight homeserver (Rust, single binary) | Like OpenSMTPD — minimal, fast |
| Element | Primary Matrix client (desktop, mobile, web) | Like Thunderbird — the user interface |
| Mautrix bridges | Bridge Matrix to other networks | Like gateway servers — protocol translation |
Which homeserver should you choose?
| Feature | Synapse | Dendrite | Conduit |
|---|
| Language | Python | Go | Rust |
| Maturity | Production-ready, since 2015 | Production-ready | Production-ready |
| Federation | Full | Full | Full |
| Resource usage | 2-4 GB RAM recommended | 512 MB - 1 GB RAM | 64-256 MB RAM |
| Multi-user scale | Thousands | Hundreds to thousands | Tens to hundreds |
| Bridge support | Extensive | Growing | Limited |
| Best for | General purpose, bridges | Resource-constrained servers | Home labs, small groups |
For most users, Synapse is the safest choice — it has the most features, best bridge support, and largest community. If you are running on a tiny VPS, Conduit offers the smallest footprint.
Deploying Synapse with docker
Here is a complete production-ready setup using Docker Compose. This configuration includes Synapse, Element Web, and a reverse proxy-ready layout.
Prerequisites
- A domain name (e.g.,
matrix.example.com) - A VPS with at least 2 GB RAM, 2 vCPUs, and 20 GB storage
- Docker and Docker Compose installed
Directory Structure
1
2
3
4
5
6
7
8
| matrix-server/
├── docker-compose.yml
├── synapse/
│ ├── homeserver.yaml
│ └── data/
├── elemen[nginx](https://nginx.org/) └── config.json
└── nginx/
└── conf.d/
|
Docker Compose
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| version: "3.8"
services:
synapse:
image: docker.io/matrixdotorg/synapse:latest
container_name: synapse
restart: unless-stopped
volumes:
- ./synapse/homeserver.yaml:/data/homeserver.yaml:ro
- ./synapse/data:/data
environment:
- SYNAPSE_SERVER_NAME=matrix.example.com
- SYNAPSE_REPORT_STATS=yes
ports:
- "8008:8008"
networks:
- matrix-net
element:
image: docker.io/vectorim/element-web:latest
container_name: element-web
restart: unless-stopped
volumes:
- ./element/config.json:/app/config.json:ro
ports:
- "8080:80"
networks:
- matrix-net
postgres:
image: docker.io/postgres:16-alpine
container_name: matrix-postgres
restart: unless-stopped
environment:
POSTGRES_USER: synapse
POSTGRES_PASSWORD: your-secure-password-here
POSTGRES_DB: synapse
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- matrix-net
networks:
matrix-net:
driver: bridge
volumes:
postgres-data:
|
Synapse Configuration
Generate the initial config with:
1
2
3
4
5
| docker run --rm -it \
-v ./synapse/data:/data \
-e SYNAPSE_SERVER_NAME=matrix.example.com \
-e SYNAPSE_REPORT_STATS=yes \
docker.io/matrixdotorg/synapse:latest generate
|
Then edit homeserver.yaml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| server_name: "matrix.example.com"
pid_file: /data/homeserver.pid
public_baseurl: https://matrix.example.com/
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
resources:
- names: [client, federation]
compress: true
database:
name: psycopg2
args:
user: synapse
password: your-secure-password-here
database: synapse
host: postgres
port: 5432
cp_min: 5
cp_max: 10
registration_shared_secret: "generate-a-long-random-string-here"
enable_registration: false
enable_registration_captcha: false
log_config: "/data/matrix.example.com.log.config"
media_store_path: "/data/media_store"
macaroon_secret_key: "generate-another-long-random-string"
form_secret: "generate-a-third-random-string"
signing_key_path: "/data/matrix.example.com.signing.key"
trusted_key_servers:
- server_name: "matrix.org"
|
Generate the secrets with:
1
2
3
4
5
6
7
8
| # Registration shared secret
python3 -c "import secrets; print(secrets.token_hex(32))"
# Macaroon secret
python3 -c "import secrets; print(secrets.token_hex(32))"
# Form secret
python3 -c "import secrets; print(secrets.token_hex(32))"
|
Element Web Configuration
Create element/config.json:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| {
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.example.com",
"server_name": "matrix.example.com"
}
},
"disable_custom_urls": false,
"disable_guests": true,
"default_theme": "dark",
"brand": "MyChat",
"branding": {
"welcome_background_url": "https://example.com/background.jpg"
},
"features": {
"feature_notifications": true,
"feature_thread": true
},
"setting_defaults": {
"baseFontSize": 15
}
}
|
Reverse Proxy with Nginx
Create an Nginx configuration for your domain:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| server {
listen 80;
server_name matrix.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
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;
client_max_body_size 50M;
# Matrix client API
location /_matrix/ {
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;
proxy_read_timeout 3600s;
}
# Element Web
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $host;
}
# Element assets (long cache)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
proxy_pass http://127.0.0.1:8080;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
|
Get TLS certificates:
1
2
| sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d matrix.example.com
|
Well-Known Delegation
For federation to work, your main domain (example.com) needs .well-known/matrix/server and .well-known/matrix/client files. If you are not running a web server on the bare domain, you can serve these from the Matrix subdomain:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # DNS: Add a CAA record and ensure matrix.example.com resolves
# Then serve well-known files from the Nginx config above
# Add to the server block in Nginx:
location /.well-known/matrix/server {
return 200 '{"m.server": "matrix.example.com:443"}';
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
}
location /.well-known/matrix/client {
return 200 '{
"m.homeserver": {
"base_url": "https://matrix.example.com"
}
}';
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
}
|
Start Everything
1
2
| cd matrix-server
docker compose up -d
|
Create your first user:
1
2
3
4
5
6
| docker exec -it synapse register_new_matrix_user \
http://localhost:8008 \
-c /data/homeserver.yaml \
-u admin \
-p your-admin-password \
-a
|
Setting Up Bridges
One of Matrix’s strongest features is bridging — connecting your self-hosted server to external messaging networks so you can manage everything from Element.
Mautrix-WhatsApp Bridge
1
2
3
4
5
6
7
8
9
| # docker-compose addition for WhatsApp bridge
mautrix-whatsapp:
image: dock.mau.dev/mautrix/whatsapp:latest
container_name: mautrix-whatsapp
restart: unless-stopped
volumes:
- ./mautrix-whatsapp:/data
networks:
- matrix-net
|
Initialize and configure:
1
2
3
4
5
6
7
8
9
10
11
| # Generate config
docker run --rm -v ./mautrix-whatsapp:/data dock.mau.dev/mautrix/whatsapp:latest
# Edit config.yaml with your Synapse details
# Key settings:
# homeserver:
# address: http://synapse:8008
# domain: matrix.example.com
# appservice:
# address: http://mautrix-whatsapp:29318
# port: 29318
|
Link your WhatsApp account:
1
2
3
4
5
6
| # Start the bridge
docker compose up -d mautrix-whatsapp
# In Element, message @whatsappbot:matrix.example.com with:
# login qr
# Scan the QR code with WhatsApp > Linked Devices
|
Mautrix-Signal Bridge
1
2
3
4
5
6
7
8
9
| mkdir -p mautrix-signal && cd mautrix-signal
docker run --rm -v $(pwd):/data dock.mau.dev/mautrix/signal:latest
# Edit config.yaml similarly, then:
docker compose up -d mautrix-signal
# Link Signal with:
# link (in chat with @signalbot:matrix.example.com)
# Scan the QR code with Signal > Settings > Linked devices
|
IRC Bridge (Matrix Appservice IRC)
1
2
3
4
5
6
7
8
| mautrix-telegram:
image: dock.mau.dev/mautrix/telegram:latest
container_name: mautrix-telegram
restart: unless-stopped
volumes:
- ./mautrix-telegram:/data
networks:
- matrix-net
|
Matrix vs Proprietary Alternatives
Here is how self-hosted Matrix compares to the platforms most people use today:
| Feature | Matrix (Self-Hosted) | Slack | Discord | WhatsApp | Telegram |
|---|
| Cost | Free (your server) | $7.25+/user/mo | Free/Nitro | Free | Free |
| Data ownership | Full (your server) | None (Salesforce) | None | None (Meta) | None |
| E2E encryption | Yes (private rooms) | No (paid add-on) | No | Yes | Optional |
| Federation | Yes (open standard) | No | No | No | Partially |
| Self-hosting | Yes | No | No | No | No |
| Bridging | Extensive | Limited | None | None | None |
| Message history | Yours to keep | Deleted on free tier | Your servers | Device-dependent | Cloud |
| File sharing limit | Configurable (your disk) | 1 GB (paid) | 25 MB (free) | 2 GB | 2 GB |
| User accounts | Unlimited | Paid per user | Unlimited | Phone number | Phone number |
| Open protocol | Yes (Apache 2.0) | No | No | No | No (MTProto) |
| Audit compliance | Full control | Limited | None | None | None |
Administration and Maintenance
Backing Up Your Server
1
2
3
4
5
6
7
8
| # PostgreSQL backup
docker exec matrix-postgres pg_dump -U synapse synapse > synapse_backup_$(date +%Y%m%d).sql
# Media store backup
tar czf media_backup_$(date +%Y%m%d).tar.gz synapse/data/media_store/
# Config backup
tar czf config_backup_$(date +%Y%m%d).tar.gz synapse/homeserver.yaml element/config.json
|
For automated backups, add this cron job:
1
2
| # Daily backup at 2 AM
0 2 * * * /opt/matrix-server/scripts/backup.sh
|
Monitoring Synapse
1
2
3
4
5
6
7
8
9
10
| # Check server health
curl http://localhost:8008/_synapse/health
# View room statistics
curl -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
"http://localhost:8008/_synapse/admin/v1/statistics"
# List users
curl -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
"http://localhost:8008/_synapse/admin/v2/users?from=0&limit=50&guests=true"
|
Room and User Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # Deactivate a user
curl -X PUT \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-d '{"deactivate": true}' \
"http://localhost:8008/_synapse/admin/v2/users/@baduser:matrix.example.com"
# Purge old media (older than 30 days)
curl -X POST \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-d '{"before_ts": '$(date -d "30 days ago" +%s)'}' \
"http://localhost:8008/_synapse/admin/v1/media/delete"
# Delete a room
curl -X DELETE \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
"http://localhost:8008/_synapse/admin/v2/rooms/!roomid:matrix.example.com"
|
For servers with more than 50 active users, consider these optimizations:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # In homeserver.yaml — tune database connection pool
database:
args:
cp_min: 10
cp_max: 20
# Enable worker mode for large deployments
worker_app: "synapse.app.generic_worker"
worker_listeners:
- name: http
port: 8034
type: http
x_forwarded: true
resources:
- names: [client]
|
For high-traffic servers, split Synapse into workers:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # docker-compose additions for worker mode
synapse-main:
environment:
SYNAPSE_WORKER: synapse.app.homeserver
synapse-federation-reader:
image: docker.io/matrixdotorg/synapse:latest
command: python -m synapse.app.generic_worker
volumes:
- ./synapse/homeserver.yaml:/data/homeserver.yaml:ro
- ./synapse/data:/data
networks:
- matrix-net
|
Security Hardening
Protect your self-hosted messaging server with these steps:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| # 1. Fail2ban for Synapse
# Create /etc/fail2ban/filter.d/synapse.conf:
cat > /etc/fail2ban/filter.d/synapse.conf << 'EOF'
[Definition]
failregex = ^.*Failed password login for user .* from <HOST>
ignoreregex =
EOF
# Create /etc/fail2ban/jail.d/synapse.conf:
cat > /etc/fail2ban/jail.d/synapse.conf << 'EOF'
[synapse]
enabled = true
port = 443
filter = synapse
logpath = /opt/matrix-server/synapse/data/homeserver.log
maxretry = 5
bantime = 3600
findtime = 600
EOF
# 2. Disable registration if you do not need public sign-up
# In homeserver.yaml:
# enable_registration: false
# 3. Rate limit the client API
# In homeserver.yaml:
# rc_messages_per_second: 0.2
# rc_message_burst_count: 10
# 4. Use a firewall
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
|
From Slack
Use the slack-dump tool to export your Slack workspace, then import into Matrix:
1
2
3
4
5
6
| # Export Slack data
slack-dump -c c[mattermost](https://mattermost.com/) --export-format mattermost
# Import via the matterbridge or a custom script
# Matrix does not have a direct Slack importer yet, but the
# export format is compatible with most migration tools
|
From Discord
The discord-chat-exporter can archive your Discord channels, which can then be imported into Matrix rooms:
1
2
3
4
5
6
| # Export Discord channel history
dotnet DiscordChatExporter.Cli.dll export \
--token "YOUR_DISCORD_TOKEN" \
--channel 123456789 \
--format PlainText \
--output discord-export.txt
|
For organizations that need a clean migration, running both platforms in parallel for 30 days with a bridge is the recommended approach.
Getting Started Checklist
The Matrix ecosystem is mature, actively developed, and backed by a non-profit foundation. Whether you are a family wanting private messaging, a team needing Slack without the data harvesting, or a community seeking federated communication — Matrix with Synapse and Element delivers a production-ready solution that puts you back in control of your conversations.
Frequently Asked Questions (FAQ)
Which one should I choose in 2026?
The best choice depends on your specific requirements:
- For beginners: Start with the simplest option that covers your core use case
- For production: Choose the solution with the most active community and documentation
- For teams: Look for collaboration features and user management
- For privacy: Prefer fully open-source, self-hosted options with no telemetry
Refer to the comparison table above for detailed feature breakdowns.
Most tools support data import/export. Always:
- Backup your current data
- Test the migration on a staging environment
- Check official migration guides in the documentation
Are there free versions available?
All tools in this guide offer free, open-source editions. Some also provide paid plans with additional features, priority support, or managed hosting.
How do I get started?
- Review the comparison table to identify your requirements
- Visit the official documentation (links provided above)
- Start with a Docker Compose setup for easy testing
- Join the community forums for troubleshooting