TL;DR: Matrix is a federated, end-to-end encrypted messaging protocol. Synapse is the reference homeserver. Element is the client. By self-hosting, you control your data: no company can read your messages, shut down your server, or change the terms. This guide walks through deploying Synapse with Docker Compose, PostgreSQL, a reverse proxy, and Element Web. You'll have a private messaging server that can federate with the entire Matrix network.
Why Self-Host Matrix?
Data sovereignty: Your messages stay on your server. No third party can access, analyze, or monetize your communications.
Control: You set the rules. Who can register, retention policies, federation settings: all under your control.
Reliability: You're not dependent on a company's uptime, policy changes, or business decisions.
Federation: Your server can communicate with the entire Matrix network, or you can run it as a closed system.
Who uses Matrix:
- French government (Tchap)
- German Bundeswehr
- NATO
- Mozilla
- Privacy-conscious individuals and families
Matrix vs. Signal vs. Session
| Feature | Matrix | Signal | Session |
|---|---|---|---|
| Self-hostable | Yes | No | No |
| Federated | Yes | No | Decentralized |
| E2EE default | DMs yes, rooms optional | Yes | Yes |
| Phone number required | No | Yes | No |
| Open source | Yes | Yes | Yes |
Requirements
Hardware
- Minimum: 1 CPU core, 1GB RAM (small personal use)
- Recommended: 2+ cores, 2GB+ RAM (multiple users)
- Storage: 20GB+ (grows with media uploads)
Software
- Linux server (Ubuntu, Debian, or similar)
- Docker and Docker Compose
- Domain name pointing to your server
- Ports 80 and 443 open (HTTPS)
- Port 8448 open (federation, optional)
Hosting Options
| Provider | Free Tier | Notes |
|---|---|---|
| Oracle Cloud | Always Free (ARM) | 4 cores, 24GB RAM free |
| DigitalOcean | $200 credit (60 days) | $6/month after |
| Vultr | $100 credit (30 days) | $5/month after |
| Hetzner | None | €4.50/month, excellent value |
Architecture Overview
Our stack includes:
- Synapse: The Matrix homeserver (stores users, rooms, messages)
- PostgreSQL: Database for Synapse (much faster than SQLite)
- Caddy: Reverse proxy with automatic HTTPS
- Element Web: Web-based Matrix client
┌─────────────────────────────────────────────────┐
│ Internet │
└────────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Caddy (Reverse Proxy) │
│ Ports 80, 443, 8448 (federation) │
└────────────────────┬────────────────────────────┘
│
┌────────────┴────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Synapse │ │ Element Web │
│ Port 8008 │ │ Port 80 │
└───────┬───────┘ └───────────────┘
│
▼
┌───────────────┐
│ PostgreSQL │
│ Port 5432 │
└───────────────┘
Step-by-Step Setup
Step 1: Prepare the Server
# Update system
sudo apt update && sudo apt upgrade -y
# Install Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker \$USER
# Install Docker Compose
sudo apt install docker-compose-plugin -y
# Log out and back in for group changes Step 2: Create Directory Structure
mkdir -p ~/matrix/synapse ~/matrix/postgres ~/matrix/element ~/matrix/caddy
cd ~/matrix Step 3: Generate Synapse Config
Replace your-domain.com with your actual domain:
docker run -it --rm \
-v \$(pwd)/synapse:/data \
-e SYNAPSE_SERVER_NAME=your-domain.com \
-e SYNAPSE_REPORT_STATS=no \
matrixdotorg/synapse:latest generate Step 4: Configure Synapse for PostgreSQL
Edit synapse/homeserver.yaml:
# Comment out the SQLite section and add:
database:
name: psycopg2
args:
user: synapse_user
password: CHANGE_THIS_PASSWORD
database: synapse_db
host: postgres
cp_min: 5
cp_max: 10
# Enable user registration (optional - disable after creating accounts)
enable_registration: true
enable_registration_without_verification: true
# Or require email verification:
# enable_registration: true
# registrations_require_3pid:
# - email Step 5: Create Docker Compose File
Create docker-compose.yml:
version: "3.8"
services:
postgres:
image: postgres:15
container_name: matrix-postgres
restart: unless-stopped
environment:
POSTGRES_USER: synapse_user
POSTGRES_PASSWORD: CHANGE_THIS_PASSWORD
POSTGRES_DB: synapse_db
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C"
volumes:
- ./postgres:/var/lib/postgresql/data
networks:
- matrix
homeserver:
image: matrixdotorg/synapse:latest
container_name: matrix-synapse
restart: unless-stopped
depends_on:
- postgres
volumes:
- ./synapse:/data
networks:
- matrix
element:
image: vectorim/element-web:latest
container_name: matrix-element
restart: unless-stopped
volumes:
- ./element/config.json:/app/config.json:ro
networks:
- matrix
caddy:
image: caddy:latest
container_name: matrix-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "8448:8448"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/data:/data
- ./caddy/config:/config
networks:
- matrix
networks:
matrix:
driver: bridge Step 6: Configure Element
Create element/config.json:
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://your-domain.com",
"server_name": "your-domain.com"
}
},
"brand": "Element",
"integrations_ui_url": "",
"integrations_rest_url": "",
"integrations_widgets_urls": [],
"default_country_code": "US",
"show_labs_settings": false,
"default_theme": "dark"
} Step 7: Configure Caddy (Reverse Proxy)
Create caddy/Caddyfile:
your-domain.com {
# Synapse client API
reverse_proxy /_matrix/* homeserver:8008
reverse_proxy /_synapse/* homeserver:8008
# Element web client
reverse_proxy /* element:80
}
your-domain.com:8448 {
# Federation API
reverse_proxy /_matrix/* homeserver:8008
} Step 8: Start Everything
cd ~/matrix
docker compose up -d
# Check logs
docker compose logs -f homeserver Step 9: Create Admin User
docker exec -it matrix-synapse register_new_matrix_user \
-u admin \
-p YOUR_SECURE_PASSWORD \
-a \
-c /data/homeserver.yaml \
http://localhost:8008 After creating accounts, disable open registration in homeserver.yaml:
enable_registration: false Then restart Synapse:
docker compose restart homeserver Federation Setup
Federation lets your server communicate with other Matrix servers (matrix.org, etc.).
DNS Records
Add these DNS records:
| Type | Name | Value |
|---|---|---|
| A | your-domain.com | Your server IP |
| SRV | _matrix._tcp.your-domain.com | 0 10 8448 your-domain.com |
Test Federation
Use the Matrix Federation Tester to verify your server is reachable.
Disable Federation (Private Server)
If you want a completely private server, add to homeserver.yaml:
federation_domain_whitelist: [] Security Hardening
Firewall Rules
# Allow only necessary ports
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP (redirect to HTTPS)
sudo ufw allow 443/tcp # HTTPS
sudo ufw allow 8448/tcp # Federation (optional)
sudo ufw enable Regular Updates
# Update containers
cd ~/matrix
docker compose pull
docker compose up -d
# Update system
sudo apt update && sudo apt upgrade -y Backups
# Stop services
docker compose down
# Backup
tar -czvf matrix-backup-$(date +%Y%m%d).tar.gz \
synapse/ postgres/ element/
# Restart
docker compose up -d Enable E2EE by Default
In homeserver.yaml:
encryption_enabled_by_default_for_room_type: all Connecting Clients
Element Web
Access at: https://your-domain.com
Element Desktop/Mobile
- Download Element from element.io/download
- Click "Sign In"
- Click "Edit" next to matrix.org
- Enter your domain:
https://your-domain.com - Sign in with your credentials
Other Clients
- FluffyChat: Mobile-focused, simpler UI
- Nheko: Desktop native client
- Cinny: Discord-like web interface
- SchildiChat: Element fork with extra features
Troubleshooting
Check Logs
docker compose logs homeserver
docker compose logs postgres
docker compose logs caddy Common Issues
Database connection failed:
- Verify PostgreSQL password matches in docker-compose.yml and homeserver.yaml
- Wait for PostgreSQL to fully start before Synapse
Federation not working:
- Check port 8448 is open
- Verify DNS SRV record
- Test with Federation Tester
Can't register users:
- Check enable_registration in homeserver.yaml
- Restart Synapse after config changes
The Bottom Line
A self-hosted Matrix server gives you complete control over your communications. Your messages are encrypted end-to-end, stored on your hardware, and governed by your rules. You can federate with the global Matrix network or run a private instance for family and friends.
The setup takes about an hour if you're comfortable with Docker. Once running, it requires minimal maintenance: just periodic updates and backups. The result is a messaging platform that no company can shut down, surveil, or monetize.