The complete guide to building your own private family chat server with Matrix Synapse

How I took back my family’s privacy and escaped Big Tech surveillance – A step-by-step tutorial


The privacy wake-up call

Let me start with an uncomfortable truth: Every message you send through WhatsApp, Facebook Messenger, Discord, or virtually any “free” messaging service is not private.

I realized this when I saw a targeted ad for a very specific product my wife and I had only discussed in a private WhatsApp chat. We hadn’t searched for it. We hadn’t mentioned it anywhere else. Just one conversation, and suddenly – ads.

That was my wake-up call.

What Big Tech knows about you (and won’t tell you)

When you use “free” messaging services, here’s what they typically collect:

πŸ“Š The data they harvest

WhatsApp (Meta/Facebook):

  • Who you message and how often
  • When you’re online and for how long
  • Group membership and participation patterns
  • Device information and IP addresses
  • Contact lists (everyone in your phone)
  • Payment transaction data
  • Your metadata is analyzed even with “end-to-end encryption”

Facebook Messenger:

  • Message content (yes, they can read your messages)
  • Photos, videos, voice messages
  • Location data
  • Contacts and social graphs
  • Everything for targeted advertising
  • Shared with third-party apps

Discord:

  • All message content (not encrypted)
  • Voice chat participation
  • Gaming activity
  • Server membership patterns
  • IP addresses and device fingerprinting

The kicker? Even when they claim “end-to-end encryption,” they often control the keys, collect metadata, and build detailed profiles about your relationships, habits, and behaviors.

🎯 What they do with it

Your private conversations become:

  • Advertising gold: Targeted ads based on private discussions
  • Behavior prediction: AI models learn your patterns
  • Social graphing: Mapping your relationships and influence
  • Data products: Sold to data brokers (legally!)
  • Government requests: Handed over without warrant in many cases
  • Training data: Used to train AI models (ChatGPT, etc.)

πŸ’° The real cost of “free”

These platforms don’t charge you money because you are the product.

  • WhatsApp made $906 million from users in 2023 (despite being “free”)
  • Meta’s average revenue per user: $40/year from your data
  • Discord’s valuation: $15 billion built on user data
  • You’re not the customer; you’re the inventory

Why this matters (more than you think)

“I have nothing to hide” is what I used to say.

Then I realized:

  • Would I want strangers reading my family group chat?
  • Do I want corporations analyzing my kids’ conversations?
  • Should algorithms decide what my wife sees from me?
  • Is my medical discussion fair game for data brokers?

Privacy isn’t about “having something to hide.” It’s about autonomy, dignity, and freedom from surveillance.

Plus, once data is collected, it’s forever:

  • Data breaches expose your history (Facebook: 533M users leaked)
  • Governments change laws about data access
  • Companies get acquired (WhatsApp β†’ Facebook, now Meta)
  • AI training uses your conversations without consent

The solution: take back control

After that targeted ad incident, I made a decision: Our family conversations belong to us, not Big Tech.

I had a few options:

Option 1: Signal – Better than most, truly encrypted, but still centralized (single point of failure, US jurisdiction, requires phone numbers)

Option 2: Pay for privacy – Services like Threema exist, but you’re still trusting another company

Option 3: Self-host – Run our own server, complete control, zero surveillance

I chose Option 3.

What I built: a truly private family chat

This is the complete story of how I set up a private Matrix Synapse homeserver for my family, where:

βœ… We control the server – It’s ours, not Facebook’s

βœ… We control the data – Encrypted, backed up by us, deleted by us

βœ… No metadata harvesting – No company analyzing “who talks to whom”

βœ… No algorithms – Messages arrive in order, unfiltered

βœ… No ads, ever – Because we’re not selling ourselves

βœ… No phone numbers required – Privacy from day one

βœ… No AI training – Our conversations stay ours

βœ… Open source – Auditable code, no backdoors

Cost? $10/month. Less than Netflix. Way more valuable.

This guide includes every command, every configuration file, every mistake I made, and exactly how to replicate this setup in a single weekend.

Privacy features of this setup

Before we dive into the technical setup, let me be clear about what makes this truly private:

πŸ” End-to-end encryption (the real kind)

  • You control the encryption keys, not a corporation
  • Messages encrypted on your device before leaving
  • Server can’t decrypt messages even if compromised
  • Works by default for direct messages and private rooms

🚫 Federation disabled (complete isolation)

  • Our server doesn’t talk to other Matrix servers
  • No external servers can see our metadata
  • No federation means no data sharing, period
  • Complete privacy bubble for family/team

πŸ”’ Zero third-party access

  • No analytics companies tracking usage
  • No ad networks profiling users
  • No cloud providers scanning content
  • No government backdoors (it’s your server!)
  • No AI training on your conversations

πŸ“Š Metadata under your control

Unlike WhatsApp’s “encrypted messages” with harvested metadata:

  • You decide who sees login times
  • You control contact lists
  • You manage all logs
  • You choose retention policies
  • You delete everything if needed

πŸ›‘οΈ Multiple security layers

  • fail2ban: Automated intrusion blocking
  • Rate limiting: Prevents brute-force attacks
  • Security headers: Hardens against common attacks
  • No public registration: Invite-only
  • SSL/TLS: Encrypted connections
  • PostgreSQL: No data leaks to third parties

πŸ” Full transparency

  • Open source code (audit yourself!)
  • You see all logs
  • You control all access
  • No hidden data collection
  • No surprise privacy policy changes

Who this is for

You should build this if you:

  • πŸ” Value privacy above convenience
  • πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ Want a safe space for family communication
  • πŸ’Ό Run a small business needing private team chat
  • πŸ₯ Discuss sensitive topics (medical, legal, financial)
  • 🌍 Live in countries with government surveillance
  • 🚫 Are tired of being the product
  • πŸ’ͺ Want to learn self-hosting and take control

You might skip this if you:

  • πŸ“± Need to message people outside your group
  • ⚑ Prioritize maximum convenience over privacy
  • πŸš€ Don’t want any server maintenance
  • πŸ’» Have zero technical skills (though this guide helps!)

What we’re building: the architecture

Internet
    ↓
[Traefik Reverse Proxy - Port 80/443]
    β”œβ”€β†’ chat.example.com/ β†’ Element Web (Web Interface)
    β”œβ”€β†’ chat.example.com/_matrix β†’ Matrix Synapse (Chat Server)
    β”œβ”€β†’ chat.example.com/.well-known/matrix β†’ Discovery Endpoints
    └─→ admin.example.com β†’ Synapse Admin Panel
         ↓
    [PostgreSQL Database]

Security:
- fail2ban: Monitors & blocks malicious IPs
- Let's Encrypt: Automatic SSL certificates
- Rate limiting: Prevents abuse
- Security headers: Hardens all responses

The Stack:

  • Matrix Synapse – The chat server (like running your own WhatsApp backend)
  • Element Web – Beautiful web interface
  • PostgreSQL 15 – Database for messages and users
  • Traefik v2.10 – Reverse proxy with automatic SSL
  • Synapse Admin – Web-based user management
  • fail2ban – Intrusion prevention system
  • Docker Compose – Orchestrates everything

Cost: $6/month VPS (2 vCPU, 4GB RAM, 80GB disk)

Difficulty: Intermediate (you should be comfortable with Linux, SSH, and Docker)

Time: 2-4 hours for initial setup


Part 1: prerequisites

What you’ll need

    1. A VPS (Virtual Private Server)

– Recommended: DigitalOcean, Linode, Hetzner, or Vultr

– Specs: 2 vCPU, 4GB RAM, 80GB SSD

– OS: Ubuntu 22.04 LTS

– Cost: ~$6/month

    1. A Domain Name

– You’ll need a domain (e.g., example.com)

– We’ll use subdomains: chat.example.com and admin.example.com

– Cost: ~$12/year

    1. DNS Access

– Ability to create A records pointing to your VPS IP

– IMPORTANT: Disable DNSSEC if enabled (it can cause SSL issues)

    1. Basic Skills

– SSH access to a Linux server

– Basic command-line navigation

– Text editing with nano or vim

– Understanding of Docker (helpful but not required)

Initial server setup

SSH into your fresh Ubuntu VPS:

ssh root@your-server-ip

Update the system:

apt update && apt upgrade -y

Install required packages:

apt install -y docker.io docker-compose git curl fail2ban

Enable Docker to start on boot:

systemctl enable docker
systemctl start docker

Enable fail2ban:

systemctl enable fail2ban
systemctl start fail2ban

Create a working directory:

cd /home/ubuntu
# Or wherever you want to keep your project

Part 2: directory structure

Create all necessary directories:

mkdir -p /home/ubuntu/data/postgres
mkdir -p /home/ubuntu/data/matrix
mkdir -p /home/ubuntu/data/element
mkdir -p /home/ubuntu/data/wellknown
mkdir -p /home/ubuntu/data/letsencrypt
mkdir -p /home/ubuntu/data/traefik/logs
mkdir -p /home/ubuntu/backups

Your directory structure will look like this:

/home/ubuntu/
β”œβ”€β”€ docker-compose.yml          # Main orchestration file
β”œβ”€β”€ .env                        # Environment variables (secrets)
β”œβ”€β”€ data/
β”‚   β”œβ”€β”€ postgres/              # PostgreSQL database files
β”‚   β”œβ”€β”€ matrix/                # Synapse homeserver data
β”‚   β”œβ”€β”€ element/               # Element Web configuration
β”‚   β”‚   └── config.json
β”‚   β”œβ”€β”€ wellknown/             # Matrix discovery files
β”‚   β”‚   β”œβ”€β”€ client            # (no .json extension!)
β”‚   β”‚   └── server            # (no .json extension!)
β”‚   β”œβ”€β”€ letsencrypt/           # SSL certificates
β”‚   β”‚   └── acme.json
β”‚   └── traefik/
β”‚       └── logs/              # Access logs (for fail2ban)
└── backups/                   # Database backups

Part 3: configuration files

File 1: .env (environment variables)

Create the .env file:

nano /home/ubuntu/.env

Add this content (replace with your values):

# Email for Let's Encrypt certificate notifications
ACME_EMAIL=your-email@example.com

# PostgreSQL database password (generate a strong one!)
POSTGRES_PASSWORD=your-secure-random-password-here-use-a-generator

To generate a secure password:

openssl rand -hex 32

Save and exit (Ctrl+X, then Y, then Enter).

Security note: This file contains secrets! Set proper permissions:

chmod 600 /home/ubuntu/.env

File 2: Docker-Compose.yml (the main orchestration)

Create the docker-compose file:

nano /home/ubuntu/docker-compose.yml

Paste this complete configuration:

services:
  postgres:
    image: postgres:15-alpine
    container_name: postgres
    restart: unless-stopped
    environment:
      - POSTGRES_DB=synapse
      - POSTGRES_USER=synapse
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_INITDB_ARGS=--encoding=UTF8 --locale=C
    volumes:
      - /home/ubuntu/data/postgres:/var/lib/postgresql/data
    networks:
      - default

  synapse:
    image: matrixdotorg/synapse:latest
    container_name: synapse
    restart: unless-stopped
    environment:
      - SYNAPSE_SERVER_NAME=chat.example.com
      - SYNAPSE_REPORT_STATS=no
      - UID=1000
      - GID=1000
    volumes:
      - /home/ubuntu/data/matrix:/data
    depends_on:
      - postgres
    labels:
      - "traefik.enable=true"
      # Matrix client-server API
      - "traefik.http.routers.synapse.rule=Host(`chat.example.com`) && PathPrefix(`/_matrix`, `/_synapse`)"
      - "traefik.http.routers.synapse.entrypoints=websecure"
      - "traefik.http.routers.synapse.tls=true"
      - "traefik.http.routers.synapse.tls.certresolver=letsencrypt"
      - "traefik.http.routers.synapse.middlewares=matrix-ratelimit@docker,security-headers@docker"
      - "traefik.http.services.synapse.loadbalancer.server.port=8008"
      # Rate limiting middleware - 10 requests/second average, burst 20
      - "traefik.http.middlewares.matrix-ratelimit.ratelimit.average=10"
      - "traefik.http.middlewares.matrix-ratelimit.ratelimit.burst=20"
    networks:
      - default

  element:
    image: vectorim/element-web:latest
    container_name: element
    restart: unless-stopped
    volumes:
      - /home/ubuntu/data/element/config.json:/app/config.json:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.element.rule=Host(`chat.example.com`)"
      - "traefik.http.routers.element.entrypoints=websecure"
      - "traefik.http.routers.element.tls=true"
      - "traefik.http.routers.element.tls.certresolver=letsencrypt"
      - "traefik.http.routers.element.priority=1"
      - "traefik.http.routers.element.middlewares=security-headers@docker"
      - "traefik.http.services.element.loadbalancer.server.port=80"
    networks:
      - default
    depends_on:
      - synapse

  wellknown:
    image: nginx:alpine
    container_name: wellknown
    restart: unless-stopped
    volumes:
      - /home/ubuntu/data/wellknown:/usr/share/nginx/html/.well-known/matrix:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.wellknown.rule=Host(`chat.example.com`) && PathPrefix(`/.well-known/matrix`)"
      - "traefik.http.routers.wellknown.entrypoints=websecure"
      - "traefik.http.routers.wellknown.tls=true"
      - "traefik.http.routers.wellknown.tls.certresolver=letsencrypt"
      - "traefik.http.routers.wellknown.priority=100"
      - "traefik.http.services.wellknown.loadbalancer.server.port=80"
      - "traefik.http.middlewares.wellknown-headers.headers.customresponseheaders.Access-Control-Allow-Origin=*"
      - "traefik.http.middlewares.wellknown-headers.headers.customresponseheaders.Content-Type=application/json"
      - "traefik.http.routers.wellknown.middlewares=wellknown-headers@docker"
    networks:
      - default

  traefik:
    image: traefik:v2.10
    container_name: traefik
    restart: unless-stopped
    command:
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
      - "--entrypoints.web.http.redirections.entryPoint.scheme=https"
      - "--accesslog=true"
      - "--accesslog.filepath=/var/log/traefik/access.log"
      - "--accesslog.format=json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /home/ubuntu/data/letsencrypt:/letsencrypt
      - /home/ubuntu/data/traefik/logs:/var/log/traefik
    networks:
      - default
    labels:
      - "traefik.enable=true"
      # Security headers middleware
      - "traefik.http.middlewares.security-headers.headers.frameDeny=true"
      - "traefik.http.middlewares.security-headers.headers.sslRedirect=true"
      - "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000"
      - "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true"
      - "traefik.http.middlewares.security-headers.headers.stsPreload=true"
      - "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
      - "traefik.http.middlewares.security-headers.headers.browserXssFilter=true"
      - "traefik.http.middlewares.security-headers.headers.referrerPolicy=same-origin"
      - "traefik.http.middlewares.security-headers.headers.permissionsPolicy=geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()"

  synapse-admin:
    image: awesometechnologies/synapse-admin:latest
    container_name: synapse-admin
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.synapse-admin.rule=Host(`admin.example.com`)"
      - "traefik.http.routers.synapse-admin.entrypoints=websecure"
      - "traefik.http.routers.synapse-admin.tls=true"
      - "traefik.http.routers.synapse-admin.tls.certresolver=letsencrypt"
      - "traefik.http.routers.synapse-admin.middlewares=admin-ratelimit@docker,security-headers@docker"
      - "traefik.http.services.synapse-admin.loadbalancer.server.port=80"
      # Rate limiting for admin panel - 5 requests/second average, burst 10
      - "traefik.http.middlewares.admin-ratelimit.ratelimit.average=5"
      - "traefik.http.middlewares.admin-ratelimit.ratelimit.burst=10"
    networks:
      - default
    depends_on:
      - synapse

networks:
  default:
    driver: bridge

IMPORTANT: Replace chat.example.com and admin.example.com with your actual domains throughout this file!

Save and exit.


File 3: generate Matrix Synapse configuration

Generate the initial Synapse configuration:

docker run -it --rm \
  -v /home/ubuntu/data/matrix:/data \
  -e SYNAPSE_SERVER_NAME=chat.example.com \
  -e SYNAPSE_REPORT_STATS=no \
  matrixdotorg/synapse:latest generate

Replace chat.example.com with your actual domain!

This creates /home/ubuntu/data/matrix/homeserver.yaml and other files.


File 4: configure Synapse for PostgreSQL

Edit the generated homeserver.yaml:

nano /home/ubuntu/data/matrix/homeserver.yaml

Find the database: section (around line 500-600) and replace the SQLite configuration with PostgreSQL:

database:
  name: psycopg2
  args:
    user: synapse
    password: your-postgres-password-from-env-file
    database: synapse
    host: postgres
    port: 5432
    cp_min: 5
    cp_max: 10

Important: Replace your-postgres-password-from-env-file with the same password you put in .env!

Scroll down and find federation<em>domain</em>whitelist: (or add it). Set it to disable federation:

# Disable federation (private server)
federation_domain_whitelist: []

Find enable_registration: and ensure it’s disabled:

# Prevent public signups
enable_registration: false

Find the push: section and ensure it’s enabled:

push:
  enabled: true

Save and exit.


File 5: configure Synapse logging

Edit the log configuration:

nano /home/ubuntu/data/matrix/chat.example.com.log.config

Find the handlers: section and add a file handler (keep the console handler too):

handlers:
  file:
    class: logging.handlers.RotatingFileHandler
    formatter: precise
    filename: /data/homeserver.log
    maxBytes: 104857600  # 100MB
    backupCount: 3
    encoding: utf8

  console:
    class: logging.StreamHandler
    formatter: precise

root:
    level: INFO
    handlers: [console, file]

Save and exit.


File 6: Element web configuration

Create the Element config:

nano /home/ubuntu/data/element/config.json

Paste this (replace domain):

{
    "default_server_config": {
        "m.homeserver": {
            "base_url": "https://chat.example.com",
            "server_name": "chat.example.com"
        }
    },
    "brand": "Element",
    "disable_custom_urls": true,
    "disable_guests": true,
    "disable_login_language_selector": false,
    "disable_3pid_login": true,
    "show_labs_settings": true,
    "room_directory": {
        "servers": [
            "chat.example.com"
        ]
    },
    "enable_presence_by_hs_url": {
        "https://chat.example.com": false
    },
    "setting_defaults": {
        "breadcrumbs": true
    },
    "jitsi": {
        "preferred_domain": "meet.jit.si"
    }
}

Replace all instances of chat.example.com with your domain!

Save and exit.


File 7: well-known discovery files

Create the client discovery file (NO .json extension!):

nano /home/ubuntu/data/wellknown/client

Paste this (replace domain):

{
  "m.homeserver": {
    "base_url": "https://chat.example.com"
  }
}

Save and exit.

Create the server discovery file (NO .json extension!):

nano /home/ubuntu/data/wellknown/server

Paste this (replace domain):

{
  "m.server": "chat.example.com:443"
}

Save and exit.

Critical: These files must NOT have .json extensions, despite containing JSON. This is a Matrix spec requirement.


Part 4: DNS configuration

Before starting the services, configure DNS:

Required DNS records

Create these A records in your DNS provider:

chat.example.com    β†’  YOUR_SERVER_IP
admin.example.com   β†’  YOUR_SERVER_IP

Important: Disable DNSSEC if it’s enabled. DNSSEC validation issues can cause Let’s Encrypt certificate failures.

Wait 5-10 minutes for DNS propagation, then verify:

nslookup chat.example.com
nslookup admin.example.com

Both should return your server’s IP address.


Part 5: launch the services

Start everything

Navigate to your project directory:

cd /home/ubuntu

Start all services:

docker compose up -d

This will:

  1. Pull all Docker images (takes a few minutes)
  2. Start PostgreSQL
  3. Initialize the Synapse database
  4. Start Synapse, Element, Traefik, and Synapse Admin
  5. Obtain Let’s Encrypt SSL certificates automatically

Monitor the startup

Watch the logs:

docker compose logs -f

You should see:

  • Traefik obtaining SSL certificates
  • Synapse connecting to PostgreSQL
  • Services starting successfully

Press Ctrl+C to exit log viewing.

Check service status

docker compose ps

All services should show “Up” status.


Part 6: create your first user

Create an admin user:

docker exec -it synapse register_new_matrix_user \
  -u admin \
  -p YourSecurePassword \
  -a \
  -c /data/homeserver.yaml \
  http://localhost:8008

Replace YourSecurePassword with an actual strong password!

The -a flag makes this user an admin.

Your username will be: @admin:chat.example.com


Part 7: test Element web

Open your browser and visit:

https://chat.example.com

You should see the Element login screen!

Login:

  • Username: admin (or @admin:chat.example.com)
  • Password: (the one you just set)

If you can log in and see the Element interface, congratulations! Your Matrix server is working.


Part 8: security hardening with fail2ban

Install fail2ban filters

Create the Matrix Synapse filter:

sudo nano /etc/fail2ban/filter.d/matrix-synapse.conf

Paste:

[Definition]
failregex = ^.*\[.*\] Failed password for .* from <HOST>.*$
            ^.*\- - \[.*\] "\w+ .* HTTP/\d\.\d" 401 .*$
ignoreregex =

Save and exit.

Create the Traefik auth filter:

sudo nano /etc/fail2ban/filter.d/traefik-auth.conf

Paste:

[Definition]
failregex = ^.*"ClientAddr":"<HOST>:\d+".*"DownstreamStatus":401.*$
            ^.*"ClientHost":"<HOST>".*"DownstreamStatus":401.*$
ignoreregex =

Save and exit.

Configure fail2ban jails

Edit the jail configuration:

sudo nano /etc/fail2ban/jail.local

Paste this complete configuration:

[DEFAULT]
# Ban hosts for one hour
bantime = 3600

# Retry limit
maxretry = 5

# Ignore localhost
ignoreip = 127.0.0.1/8 ::1

[sshd]
enabled = true
port = ssh
logpath = /var/log/auth.log

[matrix-synapse]
enabled = true
port = http,https
filter = matrix-synapse
logpath = /home/ubuntu/data/matrix/homeserver.log
maxretry = 5
findtime = 600
bantime = 3600

[traefik-auth]
enabled = true
port = http,https
filter = traefik-auth
logpath = /home/ubuntu/data/traefik/logs/access.log
maxretry = 5
findtime = 600
bantime = 3600

Save and exit.

Restart fail2ban

sudo systemctl restart fail2ban

Verify fail2ban is working

sudo fail2ban-client status

You should see 3 jails: sshd, matrix-synapse, and traefik-auth.

Check a specific jail:

sudo fail2ban-client status matrix-synapse

Part 9: access the admin panel

Visit the admin panel:

https://admin.example.com

Login with your Matrix admin credentials:

  • Homeserver URL: https://chat.example.com
  • Username: admin
  • Password: (your admin password)

From here you can:

  • Create new users
  • Reset passwords
  • View user statistics
  • Manage rooms
  • Deactivate accounts

This is much easier than using command-line tools!


Part 10: mobile app setup

iOS (iPhone/iPad)

  1. Open the App Store
  2. Search for “Element”
  3. Install the app
  4. Open Element
  5. Tap “Sign In”
  6. Tap “Edit” next to the homeserver
  7. Enter: https://chat.example.com
  8. Tap “Continue”
  9. Username: admin
  10. Password: (your password)
  11. Tap “Sign In”

Push notifications work automatically!

Android

  1. Open Google Play Store
  2. Search for “Element”
  3. Install the app
  4. Open Element
  5. Tap “Sign In”
  6. Tap “Edit” next to homeserver
  7. Enter: https://chat.example.com
  8. Tap “Continue”
  9. Enter username and password
  10. Tap “Sign In”

Desktop apps

Download from: https://element.io/download

Available for Windows, macOS, and Linux.

Setup is the same: enter https://chat.example.com as the homeserver.


Part 11: creating more users

Method 1: command line

docker exec -it synapse register_new_matrix_user \
  -u username \
  -p password \
  -c /data/homeserver.yaml \
  http://localhost:8008

Method 2: Synapse admin panel (easier!)

    1. Go to https://admin.example.com
    2. Click “Users” in sidebar
    3. Click “Create” button
    4. Fill in:

– Username: mom (becomes @mom:chat.example.com)

– Password: (secure password)

  1. Click “Create”

Give family members their credentials:

  • Username: @username:chat.example.com
  • Password: (the one you set)
  • Homeserver: https://chat.example.com

Part 12: maintenance & backups

Automatic backups

Create a backup script:

nano /home/ubuntu/backup.sh

Paste:

#!/bin/bash
BACKUP_DIR="/home/ubuntu/backups"
DATE=$(date +%Y%m%d-%H%M%S)

# Backup database
docker exec postgres pg_dump -U synapse synapse > \
  $BACKUP_DIR/synapse-$DATE.sql

# Compress
gzip $BACKUP_DIR/synapse-$DATE.sql

# Keep only last 30 days
find $BACKUP_DIR -name "synapse-*.sql.gz" -mtime +30 -delete

echo "Backup completed: synapse-$DATE.sql.gz"

Make it executable:

chmod +x /home/ubuntu/backup.sh

Add to crontab (daily at 2 AM):

crontab -e

Add this line:

0 2 * * * /home/ubuntu/backup.sh

Update services

Check for updates monthly:

cd /home/ubuntu
docker compose pull
docker compose up -d

This updates all containers to their latest versions.

View logs

# All services
docker compose logs -f

# Specific service
docker compose logs -f synapse
docker compose logs -f traefik

# Synapse log file
tail -f /home/ubuntu/data/matrix/homeserver.log

# Fail2ban log
sudo tail -f /var/log/fail2ban.log

Check disk usage

du -sh /home/ubuntu/data/*

Monitor the matrix/ directory – media files can grow over time.

Restart services

# Restart all
docker compose restart

# Restart specific service
docker compose restart synapse

Check SSL certificate expiry

echo | openssl s_client -connect chat.example.com:443 \
  -servername chat.example.com 2>/dev/null | \
  openssl x509 -noout -dates

Traefik renews automatically 30 days before expiration.


Part 13: common issues & troubleshooting

Issue 1: Element shows “incorrectly configured”

Symptoms: Element won’t connect, shows configuration error.

Fix:

    1. Check well-known endpoints:
   curl https://chat.example.com/.well-known/matrix/client
   curl https://chat.example.com/.well-known/matrix/server

Both should return JSON.

    1. Verify files have NO .json extension:
   ls -la /home/ubuntu/data/wellknown/

Should show client and server (not client.json).

    1. Restart wellknown container:
   docker compose restart wellknown element

Issue 2: can’t get SSL certificate

Symptoms: Traefik logs show certificate errors, ACME failures.

Fix:

    1. Verify DNS is correct:
   nslookup chat.example.com

Must point to your server.

    1. Check if ports 80 and 443 are open:
   sudo netstat -tlnp | grep -E ':(80|443)'
  1. If you hit Let’s Encrypt rate limits (5 failures per hour), wait an hour and try again.
  1. Disable DNSSEC in your DNS provider if enabled.

Issue 3: Synapse won’t start

Symptoms: docker compose ps shows synapse as stopped or restarting.

Fix:

    1. Check logs:
   docker compose logs synapse
    1. Common issues:

– Database password mismatch between .env and homeserver.yaml

– PostgreSQL not ready yet (wait 30 seconds and check again)

– Permissions issue on /home/ubuntu/data/matrix/

    1. Verify database connection:
   docker exec postgres psql -U synapse -d synapse -c "SELECT 1;"

Issue 4: fail2ban not banning

Symptoms: Obvious brute-force attempts not being blocked.

Fix:

    1. Test the filter against logs:
   sudo fail2ban-regex /home/ubuntu/data/matrix/homeserver.log \
     /etc/fail2ban/filter.d/matrix-synapse.conf
    1. Check jail status:
   sudo fail2ban-client status matrix-synapse
    1. Restart fail2ban:
   sudo systemctl restart fail2ban

Issue 5: push notifications not working

Symptoms: Mobile apps don’t receive notifications when app is closed.

Fix:

    1. Verify push is enabled in homeserver.yaml:
   grep -A2 "^push:" /home/ubuntu/data/matrix/homeserver.yaml

Should show enabled: true.

    1. Check Synapse logs for push gateway errors:
   docker compose logs synapse | grep -i push
  1. Ensure the Element app has notification permissions enabled in phone settings.

Part 14: performance & scaling

Current setup capacity

My setup handles:

  • 8 active users
  • ~500 messages/day
  • 50-100 media uploads/day
  • 5-10 concurrent users

Resource usage:

  • CPU: 5-10% average
  • RAM: 2.5GB used
  • Disk: 12GB after 3 months (mostly media)
  • Bandwidth: ~5GB/month

When to upgrade

Consider upgrading your VPS when:

  • CPU consistently above 70%
  • RAM usage above 3.5GB
  • Disk space below 10GB free
  • User count exceeds 50

Upgrade path: 4 vCPU, 8GB RAM, 160GB disk (~$20/month)

Media retention (optional)

To prevent unlimited media growth, add to homeserver.yaml:

media_retention:
  remote_media_lifetime: 90d  # Delete remote media after 90 days

Then restart Synapse:

docker compose restart synapse

Part 15: advanced tips

Tip 1: Redis cache (performance boost)

For 20+ users, add Redis caching. Add to docker-compose.yml:

  redis:
    image: redis:alpine
    container_name: redis
    restart: unless-stopped
    networks:
      - default

Update homeserver.yaml:

redis:
  enabled: true
  host: redis
  port: 6379

Tip 2: custom branding

Replace Element’s branding with your own in /home/ubuntu/data/element/config.json:

{
    "brand": "Family Chat",
    "default_country_code": "US",
    ...
}

Tip 3: monitoring with Netdata (optional)

Add real-time monitoring:

docker run -d --name=netdata \
  --restart=unless-stopped \
  -p 19999:19999 \
  -v /proc:/host/proc:ro \
  -v /sys:/host/sys:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  netdata/netdata

Access at: http://your-server-ip:19999

Tip 4: automated updates

Install unattended-upgrades for security patches:

apt install unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades

Tip 5: offsite backups

Use rsync to copy backups to another server:

rsync -avz /home/ubuntu/backups/ user@backup-server:/backups/matrix/

Add to your backup script.


The results: 3 months later

What’s working great

βœ… Rock-solid reliability: 99.9% uptime (only downtime was planned updates)

βœ… Happy family: Everyone’s using it daily, from my 65-year-old mom to my teenage nephew

βœ… Push notifications: Work perfectly on iOS and Android

βœ… Performance: Server barely breaks a sweat with 8 concurrent users

βœ… Security: fail2ban has blocked 200+ malicious IPs so far

βœ… Privacy: Our conversations are truly private for the first time

The privacy audit: what changed

This is the most important result. Let me be specific about what we gained:

Before (WhatsApp/Facebook Messenger):

  • ❌ Meta had access to all metadata
  • ❌ Contact lists uploaded automatically
  • ❌ “Last seen” times tracked and sold
  • ❌ Group membership data harvested
  • ❌ Photos scanned for ad targeting
  • ❌ Zero control over data retention
  • ❌ Subject to government mass surveillance programs
  • ❌ Data sold to brokers without consent
  • ❌ Used for AI training

After (Self-hosted Matrix):

  • βœ… Zero third-party access to anything
  • βœ… We control encryption keys
  • βœ… Metadata stays on our server
  • βœ… No contact list harvesting
  • βœ… Presence information private
  • βœ… We decide retention policies
  • βœ… No mass surveillance (it’s our server!)
  • βœ… No data brokers involved
  • βœ… No AI training on our data

Real-world privacy wins:

  • 🎯 No more targeted ads from private conversations
  • πŸ”’ Medical discussions stay between family members
  • πŸ’° Financial planning not sold to insurance companies
  • πŸ“Έ Kids’ photos not in Facebook’s facial recognition database
  • πŸ—ΊοΈ Location data not tracked and sold
  • πŸ“ž Call patterns not analyzed for behavior profiling

The test: I mentioned a very specific product in our family chat. Three weeks later – zero ads for it. Because no one is listening.

That simple test would have failed on WhatsApp within hours.

Minor issues we’ve had

⚠️ Learning curve: Took family members a few days to get used to Element’s interface

⚠️ Voice calls: Occasionally have connectivity issues (WebRTC can be finicky)

⚠️ Media storage: Growing at ~1GB/month, but manageable

The cost

Monthly:

  • VPS: $10
  • Domain: $1 (paid annually)
  • Total: $11/month

Compare to:

  • WhatsApp: “Free” (but your data is the product)
  • Signal: Free (but centralized)
  • Slack: $7.25/user/month (would be $58/month for us!)

Would I do it again?

Absolutely. The peace of mind knowing our family’s private moments are actually private is worth way more than $11/month.

Plus, I learned a ton about:

  • Docker orchestration
  • Reverse proxies
  • SSL certificate management
  • Intrusion detection
  • Database administration

These skills transfer to dozens of other self-hosting projects.


Security checklist

Before you consider your setup “production ready,” verify:

  • [ ] Strong passwords set for admin users
  • [ ] fail2ban is running (sudo fail2ban-client status)
  • [ ] SSL certificates are working (check in browser)
  • [ ] Security headers present (check with securityheaders.com)
  • [ ] Rate limiting configured
  • [ ] Federation disabled (if you want privacy)
  • [ ] Registration disabled (enable_registration: false)
  • [ ] Backups are running automatically
  • [ ] Firewall configured (UFW or cloud firewall)
  • [ ] SSH key authentication enabled (disable password auth)
  • [ ] Server updates enabled (unattended-upgrades)
  • [ ] .env file has restricted permissions (600)
  • [ ] PostgreSQL password is strong and unique
  • [ ] Monitoring set up (logs, disk space)

Final thoughts: why privacy matters

Three months ago, I saw a targeted ad based on a private WhatsApp conversation. That moment changed everything.

Today, we have our own private chat platform where:

  • Our conversations are actually private – No corporations listening
  • We control our data – Not Big Tech’s product
  • Zero surveillance capitalism – No ads, no profiling, no data sales
  • Complete autonomy – We decide retention, access, and policies
  • Works beautifully – All devices, push notifications, smooth UX
  • Minimal maintenance – ~10 minutes/month
  • Costs less than Netflix – $11/month for true privacy

The real victory: peace of mind

The technical setup was satisfying, but the real win is psychological.

I no longer think twice before:

  • Discussing medical issues in family chat
  • Sharing financial plans
  • Posting kids’ photos
  • Coordinating sensitive topics
  • Speaking freely without self-censorship

That freedom is priceless.

You can do this

If you’re comfortable with Linux and Docker, you can replicate this setup in a single afternoon. Every command is in this guide.

The hardest part? Deciding to start.

Once it’s running, it maintains itself. The skills you learn transfer to dozens of other self-hosting projects. The privacy you gain is permanent.

The bigger picture

Every person who leaves Big Tech’s surveillance ecosystem makes a statement: Our conversations are not your product.

Mass surveillance works because we collectively consent to it. Self-hosting is one way to say “no.”

You don’t need to be paranoid to value privacy. You just need to believe that your personal moments – your family’s laughter, your vulnerable discussions, your planning and dreaming – belong to you.

They do.

Your family’s conversations are worth protecting. Take back your data.


Quick command reference

# Start everything
docker compose up -d

# Stop everything
docker compose down

# View logs
docker compose logs -f synapse

# Create user
docker exec -it synapse register_new_matrix_user \
  -u username -p password -c /data/homeserver.yaml http://localhost:8008

# Backup database
docker exec postgres pg_dump -U synapse synapse > backup.sql

# Check fail2ban
sudo fail2ban-client status

# Update everything
docker compose pull && docker compose up -d

# Restart service
docker compose restart synapse

# Check certificate
echo | openssl s_client -connect chat.example.com:443 \
  -servername chat.example.com 2>/dev/null | openssl x509 -noout -dates

# Check disk usage
du -sh /home/ubuntu/data/*

# View banned ips
sudo fail2ban-client banned

Resources & further reading

Official Documentation:

  • Matrix.org: https://matrix.org/docs/
  • Synapse Admin Guide: https://matrix-org.github.io/synapse/latest/
  • Element Documentation: https://element.io/help
  • Docker Compose: https://docs.docker.com/compose/
  • Traefik: https://doc.traefik.io/traefik/

Communities:

  • Matrix HQ: #matrix:matrix.org (if you enable federation)
  • r/selfhosted on Reddit
  • r/matrix on Reddit

Alternative Clients:

  • FluffyChat: https://fluffychat.im/
  • Nheko: https://nheko-reborn.github.io/
  • Cinny: https://cinny.in/

Other Self-Hosting Projects to Explore:

  • Nextcloud (file storage/sharing)
  • Bitwarden (password manager)
  • Vaultwarden (lightweight Bitwarden)
  • Jellyfin (media streaming)
  • Gitea (Git hosting)

About this setup

This guide documents my personal Matrix Synapse deployment for family communication. All commands have been tested on Ubuntu 22.04 LTS with Docker 24.0+.

Privacy note: All domains, passwords, and IPs in this guide are examples. Replace them with your own values.

Disclaimer: I’m not a security expert – this setup works for my family’s needs, but always do your own research for production deployments.

Updates: This setup has been running since a long time with minimal issues. I’ll update this guide as I learn more.


Contributing & feedback

Found an error? Have a suggestion? Want to share your setup?

I’d love to hear about your experience! Leave a comment below or reach out.

Tags: #selfhosted #matrix #synapse #docker #privacy #encryption #tutorial #familytech #opensource


Did this guide help you? Consider sharing it with others who want to take back control of their communications!

Leave a Reply

Your email address will not be published. Required fields are marked *