Why Matrix, why now?
Matrix is an open protocol for real-time messaging. The two pieces you’ll run are:
- Synapse (the Matrix homeserver): users, rooms, federation, auth, storage.
- Element (the client): the web app people actually use to chat.
You can absolutely self-host this. You can also absolutely spend a weekend in TLS/DNS/Reverse-proxy purgatory if you’re not careful. Let’s do it the sane way.
What you’ll set up
- A VPS/server with Linux
- A domain, with:
-
matrix.example.com→ Synapse -
element.example.com→ Element (web)
-
- Nginx as reverse proxy
- Let’s Encrypt TLS certificates
- Synapse running in Docker
This tutorial assumes:
- Ubuntu 22.04+
- You have SSH access and a domain name
Step 0: Pick your domain + DNS
You need two DNS records:
-
A/AAAA record:
matrix.example.com→ your server IP -
A/AAAA record:
element.example.com→ your server IP
Give DNS a minute to propagate.
Step 1: Install prerequisites
sudo apt update
sudo apt install -y docker.io docker-compose-plugin nginx certbot python3-certbot-nginx
sudo systemctl enable --now docker nginx
Step 2: Generate Synapse config (Docker)
Create a working directory:
mkdir -p ~/matrix/synapse
cd ~/matrix/synapse
Generate Synapse configuration (replace example.com):
docker run -it --rm \
-v "$(pwd)":/data \
-e SYNAPSE_SERVER_NAME=example.com \
-e SYNAPSE_REPORT_STATS=no \
matrixdotorg/synapse:latest generate
This creates homeserver.yaml and keys in ~/matrix/synapse.
Step 3: Create a docker-compose.yml for Synapse
Create docker-compose.yml:
services:
synapse:
image: matrixdotorg/synapse:latest
container_name: synapse
restart: unless-stopped
volumes:
- ./data:/data
environment:
- SYNAPSE_CONFIG_PATH=/data/homeserver.yaml
ports:
- "8008:8008"
Now, move your generated config into ./data (Synapse expects /data inside the container):
mkdir -p data
# If generate already wrote into ./data, skip this.
# Otherwise, move the generated files:
shopt -s dotglob
mv homeserver.yaml *signing.key data/ 2>/dev/null || true
Start Synapse:
docker compose up -d
Synapse is now listening on http://127.0.0.1:8008 (we’ll put Nginx in front).
Step 4: Nginx reverse proxy for Synapse
Create an Nginx site for matrix.example.com:
sudo nano /etc/nginx/sites-available/matrix
Paste:
server {
listen 80;
server_name matrix.example.com;
location / {
proxy_pass http://127.0.0.1:8008;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Enable it:
sudo ln -s /etc/nginx/sites-available/matrix /etc/nginx/sites-enabled/matrix
sudo nginx -t
sudo systemctl reload nginx
Step 5: Add HTTPS (Let’s Encrypt)
sudo certbot --nginx -d matrix.example.com
Follow prompts. Certbot updates Nginx to serve HTTPS.
Step 6: Set up Element Web
Element Web is just static files served by a web server. Easiest: run it in Docker and proxy it.
Create a directory:
mkdir -p ~/matrix/element
cd ~/matrix/element
Create docker-compose.yml:
services:
element:
image: vectorim/element-web:latest
container_name: element
restart: unless-stopped
ports:
- "8080:80"
volumes:
- ./config.json:/app/config.json:ro
Create config.json (replace domains):
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.example.com",
"server_name": "example.com"
}
},
"disable_custom_urls": false,
"disable_guests": true,
"default_theme": "light"
}
Start Element:
docker compose up -d
Element is now at http://127.0.0.1:8080.
Step 7: Nginx reverse proxy + HTTPS for Element
Create:
sudo nano /etc/nginx/sites-available/element
Paste:
server {
listen 80;
server_name element.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Enable + reload:
sudo ln -s /etc/nginx/sites-available/element /etc/nginx/sites-enabled/element
sudo nginx -t
sudo systemctl reload nginx
Add HTTPS:
sudo certbot --nginx -d element.example.com
Now you should be able to open:
https://element.example.com
…and point it at your homeserver.
Step 8: Create your first Matrix user
By default, Synapse disables open registration (good). Create an admin user:
docker exec -it synapse register_new_matrix_user \
-c /data/homeserver.yaml \
http://localhost:8008
Follow prompts. You can create additional non-admin users the same way.
Step 9 (Important): Don’t forget the boring-but-deadly parts
Self-hosting isn’t hard, it’s just annoying in exactly the places you can’t afford to mess up:
- Backups (Synapse data + database if you add Postgres)
- Updates (Synapse and Element)
- Monitoring logs
- Firewalling and SSH hardening
- Rate-limits and anti-spam settings
If you’re doing this for a community, these are not optional.
Want to save yourself the headache?
If you read the steps above and thought “I’d rather eat glass than debug Nginx + TLS at 2am,” here’s the spicy truth: you can skip most of this pain.
Using https://clawship.app, you can spin up Matrix + Element fast and also get your own bot interaction without babysitting the plumbing. Same end goal, less yak-shaving.
(If you like yak-shaving, carry on. No judgment. Mild concern, maybe.)
Quick troubleshooting
-
Element loads but can’t connect: confirm
config.jsonpoints tohttps://matrix.example.com. - 502 from Nginx: verify containers are running:
docker ps
- Certbot fails: DNS record is wrong or not propagated.
- Synapse feels slow: consider switching to Postgres (recommended for real use).
Wrap-up
You now have:
- A working Matrix homeserver (Synapse)
- Element Web served securely over HTTPS
- A foundation you can harden and scale
If you want, tell me your domain + whether you want Postgres and I’ll provide a production-grade compose setup (Synapse + Postgres + Redis, plus safer Nginx headers).
Top comments (0)