# Self-Hosting Matrix

> Building Your Own Federated Chat Server

By Zsolt Bizderi · Published 2026-11-06
Canonical: https://ambientnode.uk/self-hosting-matrix

I've been pulling my communications off third-party platforms for years, and messaging was the last big holdout. Initially moved from FB to WhatsApp in 2018, then left WhatsApp for Signal in 2021 following their infamous privacy policy update. Signal has been my daily driver since, but I still actively need to check WhatsApp. This weekend I finally deployed a Matrix homeserver, and it's now handling my personal chat, a couple of family rooms, and bridges into networks I can't convince people to leave.

## What is Matrix?

[Matrix](https://matrix.org/) is an open protocol for decentralised, real-time communication. The easiest way to think of it is "email, but for chat". With email, you can have an account on any provider and still message anyone else, because everyone uses SMTP. Matrix does the same for instant messaging: your account lives on a homeserver, and that homeserver federates with every other homeserver in the world over HTTPS.

A few interesting features:

* **Federation:** Rooms are replicated across every server with a participant in them. There's no SPOF and no company that can delete a community overnight.
* **End-to-end encryption:** E2EE (based Olm/Megolm, similar to Signal's protocol) is on by default for private conversations.
* **Bridging:** Matrix can bridge to WhatsApp, Telegram, Discord, IRC, Signal and more, letting you consolidate everything into one client.
* **Open ecosystem.** Multiple servers (Synapse, Dendrite, Conduit), multiple clients (Element, FluffyChat, Cinny), and a spec anyone can implement.

The reference server implementation is **Synapse**, written in Python and maintained by [Element](https://github.com/element-hq/synapse). It's the most feature-complete option and the one I deployed.

## Why bother self-hosting it?

1. **Family chat that isn't owned by Meta:** Photos of the kids don't need to transit an advertising company's infrastructure.
2. **Homelab notifications:** Anything that can send a webhook can post into a Matrix room. My alerts are sent to one place.
3. **Bridging:** A single client for the two or three networks I'm stuck on for other people's sake.
4. **A comms channel I control end to end: I**f a platform changes its terms, raises prices, or vanishes, Matrix rooms and history are unaffected.

## The build

My setup: a small VPS (2 vCPU / 4GB RAM works fine), Ubuntu 24.04, Docker Compose, PostgreSQL as the db, and Nginx as the reverse proxy with a Let's Encrypt certificate. The homeserver will live at `matrix.example.uk`, but user IDs will be the cleaner `@zsolt:example.uk` thanks to well-known delegation.

### How it comes together:

* **Synapse** listens on port 8008 (plain HTTP, internal only) and does all the actual Matrix work.
* **PostgreSQL** stores everything (events, device keys, media metadata). SQLite technically works but I prefer Postgres.
* **Nginx** terminates TLS on 443 for clients and 8448 for federation, proxying both to Synapse.
* **Delegation** lets you use `@user:example.uk` identities while the server actually runs on `matrix.example.uk`. Other servers fetch `https://example.uk/.well-known/matrix/server` to discover where to send federation traffic.

### Step 1: DNS and prerequisites

Create an A record for `matrix.example.uk` pointing at your server, then install the essentials:

```
sudo apt update && sudo apt upgrade -y
sudo apt install -y docker.io docker-compose-v2 nginx certbot python3-certbot-nginx
sudo systemctl enable --now docker
```

### Step 2: Generate the Synapse config

Synapse has its own config generator we need to run once to produce `homeserver.yaml` and signing keys:

```
sudo mkdir -p /opt/matrix/synapse
cd /opt/matrix

docker run -it --rm \
  -v /opt/matrix/synapse:/data \
  -e SYNAPSE_SERVER_NAME=example.uk \
  -e SYNAPSE_REPORT_STATS=no \
  matrixdotorg/synapse:latest generate
```

Note that `SYNAPSE_SERVER_NAME` is your identity domain (`example.uk`), not the subdomain the server runs on. This is baked into every user ID and room ID permanently and you cannot change it later without starting over.

### Step 3: Docker Compose stack

Create `/opt/matrix/docker-compose.yml`:

```
services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: synapse
      POSTGRES_PASSWORD: change-me-to-something-long
      POSTGRES_DB: synapse
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
    volumes:
      - ./postgres:/var/lib/postgresql/data

  synapse:
    image: matrixdotorg/synapse:latest
    restart: unless-stopped
```

Now edit `/opt/matrix/synapse/homeserver.yaml` and swap the default SQLite block for Postgres:

```
database:
  name: psycopg2
  args:
    user: synapse
    password: change-me-to-something-long
    database: synapse
    host: postgres
    cp_min: 5
    cp_max: 10
```

Ensure these two settings are set:

```
enable_registration: false
registration_shared_secret: "generate-a-long-random-string"
```

Leave open registration off unless you want to host spam bots. The shared secret lets you create accounts from the CLI instead.
Spin it up:

```
cd /opt/matrix
docker compose up -d
curl http://127.0.0.1:8008/_matrix/client/versions
```

### Step 4: Nginx and TLS

Get a certificate and create the vhost:

```
sudo certbot certonly --nginx -d matrix.example.uk
```

`/etc/nginx/sites-available/matrix`:

```
server {
    listen 443 ssl http2;
    listen 8448 ssl http2 default_server;
    server_name matrix.example.uk;

    ssl_certificate /etc/letsencrypt/live/matrix.example.uk/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/matrix.example.uk/privkey.pem;

    location ~ ^(/_matrix|/_synapse/client) {
        proxy_pass http://127.0.0.1:8008;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $host;
        client_max_body_size 50M;
    }
}
```

```
sudo ln -s /etc/nginx/sites-available/matrix /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```

Port 8448 is the default federation port. Open 443 and 8448 in your firewall.

### Step 5: Delegation

On whatever serves your apex domain (`example.uk`), publish two JSON files:
`https://example.uk/.well-known/matrix/server`:

```
{ "m.server": "matrix.example.uk:443" }
```

`https://example.uk/.well-known/matrix/client`:

```
{ "m.homeserver": { "base_url": "https://matrix.example.uk" } }
```

Serve them with `Content-Type: application/json` and an `Access-Control-Allow-Origin: *` header on the client file, this allows you to have short user IDs while the server runs on a subdomain. If you delegate via well-known on 443, you likely don't even need 8448 exposed.

### Step 6: Create your account and verify

```
docker exec -it matrix-synapse-1 register_new_matrix_user \
  -u zsolt -a -c /data/homeserver.yaml http://localhost:8008
```

The `-a` flag makes the user admin. Then run your domain through the [Matrix Federation Tester](https://federationtester.matrix.org). Log in with Element (web, desktop, or mobile), connect at `example.uk`, and you're federated with the entire Matrix network.

## Reflections after a week

**The good.** It just works great and federation is neat to see in action when someone from `matrix.org` joins a room hosted on your own server. Resource usage on a personal server is modest with Synapse idling around 400-500MB of RAM for me, spiking only when it first joins large federated rooms. E2EE in Element is works well enough that non-technical family members haven't noticed it's there.

**The less good.** Joining a huge public room (Matrix HQ, for instance) hammers the server for several minutes while it backfills state. And Synapse's database grows quickly, so schedule media retention and set `forgotten_room_retention_period` early rather than discovering a 40GB Postgres volume later. Also, you cannot use a CF tunnel for this as federation requires port 8448 to be available. You could potentially front it with Pangolin though, so no port forwarding is needed at all.

**Would I recommend it?** If you're comfortable running any self-hosted service, absolutely. The stack is three containers and an Nginx vhost, way less complex than a lot of things in the average homelab, and unlike most of them, it replaces something you'd otherwise "rent" from a company whose incentives aren't yours.
