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

  1. Download Element from element.io/download
  2. Click "Sign In"
  3. Click "Edit" next to matrix.org
  4. Enter your domain: https://your-domain.com
  5. 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.

References

  1. Matrix.org: Understanding Synapse Hosting
  2. Docker Hub: Official Synapse Image
  3. GitHub: Matrix On-Premise Docker Setup
  4. Element: Official Matrix Client
  5. SelfHostBlog: Complete Matrix Synapse Guide