DEV Community

El Housseine Jaafari
El Housseine Jaafari

Posted on

Host Your Own Matrix + Element on a Server (Without Losing Your Weekend)

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
Enter fullscreen mode Exit fullscreen mode

Step 2: Generate Synapse config (Docker)

Create a working directory:

mkdir -p ~/matrix/synapse
cd ~/matrix/synapse
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Start Synapse:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Enable it:

sudo ln -s /etc/nginx/sites-available/matrix /etc/nginx/sites-enabled/matrix
sudo nginx -t
sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

Step 5: Add HTTPS (Let’s Encrypt)

sudo certbot --nginx -d matrix.example.com
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Start Element:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Enable + reload:

sudo ln -s /etc/nginx/sites-available/element /etc/nginx/sites-enabled/element
sudo nginx -t
sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

Add HTTPS:

sudo certbot --nginx -d element.example.com
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.json points to https://matrix.example.com.
  • 502 from Nginx: verify containers are running:
  docker ps
Enter fullscreen mode Exit fullscreen mode
  • 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)