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
-
- 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
-
- 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
-
- DNS Access
– Ability to create A records pointing to your VPS IP
– IMPORTANT: Disable DNSSEC if enabled (it can cause SSL issues)
-
- 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:
- Pull all Docker images (takes a few minutes)
- Start PostgreSQL
- Initialize the Synapse database
- Start Synapse, Element, Traefik, and Synapse Admin
- 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)
- Open the App Store
- Search for “Element”
- Install the app
- Open Element
- Tap “Sign In”
- Tap “Edit” next to the homeserver
- Enter:
https://chat.example.com
- Tap “Continue”
- Username:
admin
- Password: (your password)
- Tap “Sign In”
Push notifications work automatically!
Android
- Open Google Play Store
- Search for “Element”
- Install the app
- Open Element
- Tap “Sign In”
- Tap “Edit” next to homeserver
- Enter:
https://chat.example.com
- Tap “Continue”
- Enter username and password
- 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!)
-
- Go to
https://admin.example.com
- Click “Users” in sidebar
- Click “Create” button
- Fill in:
- Go to
– Username: mom
(becomes @mom:chat.example.com
)
– Password: (secure password)
- 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:
-
- 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.
-
- Verify files have NO
.json
extension:
- Verify files have NO
ls -la /home/ubuntu/data/wellknown/
Should show client
and server
(not client.json
).
-
- Restart wellknown container:
docker compose restart wellknown element
Issue 2: can’t get SSL certificate
Symptoms: Traefik logs show certificate errors, ACME failures.
Fix:
-
- Verify DNS is correct:
nslookup chat.example.com
Must point to your server.
-
- Check if ports 80 and 443 are open:
sudo netstat -tlnp | grep -E ':(80|443)'
- If you hit Let’s Encrypt rate limits (5 failures per hour), wait an hour and try again.
- 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:
-
- Check logs:
docker compose logs synapse
-
- 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/
-
- 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:
-
- Test the filter against logs:
sudo fail2ban-regex /home/ubuntu/data/matrix/homeserver.log \
/etc/fail2ban/filter.d/matrix-synapse.conf
-
- Check jail status:
sudo fail2ban-client status matrix-synapse
-
- Restart fail2ban:
sudo systemctl restart fail2ban
Issue 5: push notifications not working
Symptoms: Mobile apps don’t receive notifications when app is closed.
Fix:
-
- Verify push is enabled in homeserver.yaml:
grep -A2 "^push:" /home/ubuntu/data/matrix/homeserver.yaml
Should show enabled: true
.
-
- Check Synapse logs for push gateway errors:
docker compose logs synapse | grep -i push
- 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!