DEV Community

El Housseine Jaafari
El Housseine Jaafari

Posted on

Self‑Hosting Matrix + Element on Your Own Server (The Friendly, Spicy Tutorial)

Why you’d do this (and why it’s a bit of a rite of passage)

Matrix is an open, decentralized communication protocol. Element is the most common client for it. Self‑hosting them is appealing when you want:

  • Ownership over your comms stack
  • Your own domain + branding
  • Data residency / compliance control
  • The satisfaction of running real infrastructure

It’s also… a small pile of moving parts: DNS, TLS, reverse proxies, federation ports, well‑known files, and making sure your homeserver doesn’t melt the moment you enable registration.

This guide walks you through a practical, beginner‑friendly setup using Docker Compose and Synapse (the reference Matrix homeserver).


What you’ll build

By the end you’ll have:

  • A Matrix homeserver at: matrix.yourdomain.com
  • Element Web at: chat.yourdomain.com
  • HTTPS everywhere (Let’s Encrypt)
  • A working client login and room chat

Assumptions

  • You own a domain, e.g. yourdomain.com
  • You have a VPS/server (Ubuntu/Debian‑like)
  • You can open ports 80 and 443 to the server

Step 0: DNS (don’t skip this, future‑you will cry)

Create DNS records:

  • matrix.yourdomain.com → your server IP (A/AAAA)
  • chat.yourdomain.com → your server IP (A/AAAA)

You can put both behind the same reverse proxy; we’ll do that.


Step 1: Install Docker + Compose

On most modern distros:

sudo apt update
sudo apt install -y docker.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
Enter fullscreen mode Exit fullscreen mode

Log out and back in so your user is in the docker group.


Step 2: Pick a reverse proxy (Caddy keeps your blood pressure lower)

You can do this with Nginx + Certbot, Traefik, etc. For a clean tutorial, Caddy is hard to beat: automatic TLS, simple config.

We’ll run:

  • caddy (reverse proxy + TLS)
  • synapse (Matrix homeserver)
  • element-web (web client)
  • postgres (recommended DB for Synapse)

Create a working directory:

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

Step 3: Generate Synapse config

Synapse needs a config file + keys.

Create a docker-compose.yml we’ll fill in (don’t run it yet):

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: synapse
      POSTGRES_PASSWORD: CHANGE_ME_STRONG
      POSTGRES_DB: synapse
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    restart: unless-stopped

  synapse:
    image: matrixdotorg/synapse:latest
    environment:
      SYNAPSE_SERVER_NAME: yourdomain.com
      SYNAPSE_REPORT_STATS: "no"
    volumes:
      - ./data/synapse:/data
    depends_on:
      - postgres
    restart: unless-stopped

  element-web:
    image: vectorim/element-web:latest
    volumes:
      - ./data/element/config.json:/app/config.json:ro
    restart: unless-stopped

  caddy:
    image: caddy:2
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./data/caddy:/data
      - ./data/caddy-config:/config
    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

Now generate Synapse config and keys:

docker run -it --rm \
  -v "$(pwd)/data/synapse:/data" \
  -e SYNAPSE_SERVER_NAME=yourdomain.com \
  -e SYNAPSE_REPORT_STATS=no \
  matrixdotorg/synapse:latest generate
Enter fullscreen mode Exit fullscreen mode

This creates data/synapse/homeserver.yaml and signing keys.

Configure Synapse to use Postgres

Edit data/synapse/homeserver.yaml and set the database section (replace the default sqlite3):

database:
  name: psycopg2
  args:
    user: synapse
    password: "CHANGE_ME_STRONG"
    database: synapse
    host: postgres
    cp_min: 5
    cp_max: 10
Enter fullscreen mode Exit fullscreen mode

Also set the public base URL:

public_baseurl: "https://matrix.yourdomain.com/"
Enter fullscreen mode Exit fullscreen mode

Step 4: Configure Element Web

Create data/element/config.json:

{
  "default_server_config": {
    "m.homeserver": {
      "base_url": "https://matrix.yourdomain.com",
      "server_name": "yourdomain.com"
    }
  },
  "disable_custom_urls": true,
  "disable_guests": true,
  "brand": "Your Matrix"
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Reverse proxy + TLS with Caddy

Create Caddyfile:

matrix.yourdomain.com {
  reverse_proxy synapse:8008
}

chat.yourdomain.com {
  reverse_proxy element-web:80
}
Enter fullscreen mode Exit fullscreen mode

Synapse listens on 8008 inside the container by default.

Start everything:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Check logs if needed:

docker compose logs -f --tail=200
Enter fullscreen mode Exit fullscreen mode

Step 6: Create your first user

Exec into the synapse container and register a user.

docker compose exec synapse register_new_matrix_user \
  -c /data/homeserver.yaml \
  http://localhost:8008
Enter fullscreen mode Exit fullscreen mode

Follow the prompts (username/password). Then open:

  • Element Web: https://chat.yourdomain.com

Log in with:

  • username: @youruser:yourdomain.com

Step 7 (important): Lock it down a bit

Self‑hosting chat is fun until it becomes a spam magnet.

Consider:

  • Disable open registration unless you truly want it
  • Enable reCAPTCHA / token‑based registration if you must allow signups
  • Keep Synapse updated
  • Monitor disk space (media can grow fast)
  • Back up Postgres + Synapse keys (data/synapse)

In homeserver.yaml, check:

enable_registration: false
Enter fullscreen mode Exit fullscreen mode

Federation and “well‑known” (optional, but good to know)

If you want @user:yourdomain.com to work nicely and support federation cleanly, you’ll eventually run into:

  • .well-known/matrix/server
  • .well-known/matrix/client
  • Potentially opening port 8448 (depends on your setup)

This is where the “I just wanted to chat with friends” journey turns into “I am now operating a small internet service.” Congrats.

If you want a follow‑up post, I can provide a clean, battle‑tested .well-known + federation configuration.


The honest part: this is a headache you don’t have to keep

If you love tinkering, self‑hosting Matrix is rewarding.

If you’d rather skip the yak‑shaving and still get:

  • Matrix + Element on your own domain
  • A managed path to add a bot and interact with it
  • A smoother “it just works” experience

…you can save yourself the whole weekend by using https://clawship.app.

Clawship is built to help you spin up your stack faster and add your own bot interaction without reinventing the infrastructure wheel. You focus on the community and automation, not the reverse proxy sudoku.


Troubleshooting quick hits

  • Caddy won’t get certificates → check DNS points to the server and ports 80/443 are open
  • Synapse errors on DB → verify Postgres credentials match in homeserver.yaml
  • Element loads but can’t login → confirm Element config points at https://matrix.yourdomain.com
  • Media storage exploding → set retention policies and monitor disk usage

What’s next

If you want, I can write a follow‑up tutorial on:

  • Federation + .well-known done correctly
  • Adding a Matrix bot (and safe bot permissions)
  • Moderation + anti‑spam strategy
  • Backups and disaster recovery

And if you’d rather just ship: https://clawship.app.

Top comments (0)