← Back to posts
comparison guide self-hosted · · 12 min read

Matrix vs Synapse vs Element: Complete Self-Hosted Messaging Guide 2026

Complete guide to setting up Matrix (Synapse or Dendrite) with Element for self-hosted, end-to-end encrypted messaging. Covers Docker deployment, federation, bridging, and comparison with proprietary alternatives.

OS
Editorial Team

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.

Why Self-Host Your Messaging Platform

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:

ComponentRoleAnalogy
Matrix ProtocolOpen standard for decentralized communicationLike SMTP for email — defines how servers talk
SynapseReference homeserver implementation (Python)Like Postfix — the actual server software
DendriteSecond-gen homeserver (Go, monolithic or polylithic)Like Exim — lighter alternative server
ConduitLightweight homeserver (Rust, single binary)Like OpenSMTPD — minimal, fast
ElementPrimary Matrix client (desktop, mobile, web)Like Thunderbird — the user interface
Mautrix bridgesBridge Matrix to other networksLike gateway servers — protocol translation

Which homeserver should you choose?

FeatureSynapseDendriteConduit
LanguagePythonGoRust
MaturityProduction-ready, since 2015Production-readyProduction-ready
FederationFullFullFull
Resource usage2-4 GB RAM recommended512 MB - 1 GB RAM64-256 MB RAM
Multi-user scaleThousandsHundreds to thousandsTens to hundreds
Bridge supportExtensiveGrowingLimited
Best forGeneral purpose, bridgesResource-constrained serversHome 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:

FeatureMatrix (Self-Hosted)SlackDiscordWhatsAppTelegram
CostFree (your server)$7.25+/user/moFree/NitroFreeFree
Data ownershipFull (your server)None (Salesforce)NoneNone (Meta)None
E2E encryptionYes (private rooms)No (paid add-on)NoYesOptional
FederationYes (open standard)NoNoNoPartially
Self-hostingYesNoNoNoNo
BridgingExtensiveLimitedNoneNoneNone
Message historyYours to keepDeleted on free tierYour serversDevice-dependentCloud
File sharing limitConfigurable (your disk)1 GB (paid)25 MB (free)2 GB2 GB
User accountsUnlimitedPaid per userUnlimitedPhone numberPhone number
Open protocolYes (Apache 2.0)NoNoNoNo (MTProto)
Audit complianceFull controlLimitedNoneNoneNone

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"

Performance Tuning

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

Migration Paths from Other Platforms

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

  • Register a domain name and point it to your VPS
  • Install Docker and Docker Compose
  • Deploy Synapse + PostgreSQL + Element Web
  • Configure Nginx reverse proxy with TLS
  • Set up .well-known delegation files
  • Create admin account
  • Disable open registration (if private)
  • Configure automated backups
  • Set up Fail2ban for brute-force protection
  • Deploy bridges for external networks you need
  • Test federation by joining #matrix:matrix.org
  • Configure Element push notifications for mobile

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.

Can I migrate between these tools?

Most tools support data import/export. Always:

  1. Backup your current data
  2. Test the migration on a staging environment
  3. 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?

  1. Review the comparison table to identify your requirements
  2. Visit the official documentation (links provided above)
  3. Start with a Docker Compose setup for easy testing
  4. Join the community forums for troubleshooting
Advertise here