How to Run Your Own Chat Server with Matrix (Conduit and Element, Self-Hosted)

Your own chat server: Matrix with Conduit and Element. End-to-end encrypted, on your domain, talks to everyone.

Matrix is an open chat protocol: servers run by anyone, talking to each other, with end-to-end encryption, voice and video calls, and apps for every platform. Run your own and the family's group chat lives on your server, with addresses like @sarah:example.com, while still able to message people on other Matrix servers. The reference server (Synapse) is heavy; Conduit does the same job for a household in a tiny container, and Element is the polished client.

Quick reference

PieceWhat
Servermatrixconduit/matrix-conduit on port 6167, behind matrix.example.com
Clientvectorim/element-web at chat.example.com (or just use the Element apps)
Domainuser IDs are @name:example.com; two .well-known files on example.com point at matrix.example.com
Registrationa token, so only people you give it to can sign up
AppsElement (iOS, Android, desktop), FluffyChat, Cinny
Checkfederationtester.matrix.org

1. The compose file

mkdir -p ~/docker/matrix && cd ~/docker/matrix
nano compose.yaml

services:
conduit:
image: matrixconduit/matrix-conduit:latest
container_name: conduit
ports:
- "127.0.0.1:6167:6167"
environment:
CONDUIT_SERVER_NAME: example.com
CONDUIT_DATABASE_PATH: /var/lib/matrix-conduit/
CONDUIT_DATABASE_BACKEND: rocksdb
CONDUIT_PORT: 6167
CONDUIT_MAX_REQUEST_SIZE: 20000000
CONDUIT_ALLOW_REGISTRATION: "true"
CONDUIT_REGISTRATION_TOKEN: ${REG_TOKEN}
CONDUIT_ALLOW_FEDERATION: "true"
CONDUIT_TRUSTED_SERVERS: '["matrix.org"]'
CONDUIT_ADDRESS: 0.0.0.0
volumes:
- ./db:/var/lib/matrix-conduit/
restart: unless-stopped

element:
image: vectorim/element-web:latest
container_name: element
ports:
- "127.0.0.1:8070:80"
volumes:
- ./element-config.json:/app/config.json
restart: unless-stopped

CONDUIT_SERVER_NAME is the part after the : in usernames — your bare domain. It can never be changed later. REG_TOKEN in .env (a long random string): registration is open only to people who know it.

nano element-config.json

{
"default_server_config": {
"m.homeserver": { "base_url": "https://matrix.example.com", "server_name": "example.com" }
},
"brand": "Family Chat"
}

docker compose up -d

2. Reverse proxy

Two server blocks (tutorial 28), both with HTTPS:

Federation (other servers reaching yours) also needs matrix.example.com on port 8448 or the well-known file below — the file is easier.

3. The well-known files

On whatever serves https://example.com (your main site), two tiny JSON responses:

https://example.com/.well-known/matrix/server:

{ "m.server": "matrix.example.com:443" }

https://example.com/.well-known/matrix/client:

{ "m.homeserver": { "base_url": "https://matrix.example.com" } }

In Nginx that's a location /.well-known/matrix/server { default_type application/json; return 200 '{"m.server":"matrix.example.com:443"}'; } block (and the same for /client, plus add_header Access-Control-Allow-Origin *;). Then check federationtester.matrix.org with example.com — all green means other servers can reach you.

4. Create accounts

Open https://chat.example.com → Create account → username, password, and the registration token. Make yours first. Then, in any room, the server admin room #admins:example.com accepts commands like !admin list-users.

Family members install Element on their phones, choose Edit homeserver → example.com, and register with the token. Turn CONDUIT_ALLOW_REGISTRATION to "false" once everyone's in.

5. Use it

Create a room "Family", invite @sarah:example.com, turn on encryption (default for private rooms), send photos, start a voice call. Invite someone on another server by their full address (@friend:matrix.org) — federation handles it. Bridges (to WhatsApp, Signal, Telegram) exist as separate containers if you want one app for everything.

Backup

./db is everything. Stop the container for a consistent copy, or rely on nightly snapshots (tutorial 97) — RocksDB tolerates point-in-time copies reasonably well, but a stopped copy is safest. Encryption keys live on the devices; each user should set up Secure Backup in Element so a lost phone doesn't lose history.

Troubleshooting

Federation tester fails on the server record. The well-known file isn't served over HTTPS on the bare domain, or returns HTML. curl https://example.com/.well-known/matrix/server.

Can't send images over ~1 MB. client_max_body_size in the proxy and CONDUIT_MAX_REQUEST_SIZE.

"Unable to verify" / encryption errors on a new device. Verify the device from another logged-in session, or restore from Secure Backup.

Changed the server name and everything broke. Expected — it's permanent. Restore ./db or start a fresh server.

Wrapping up

Two containers, two proxy entries, two well-known files, a registration token. The household chats on @name:example.com with encryption on by default, calls work, and anyone on the wider Matrix network can still reach you — on a server that fits in 100 MB of RAM.

Call us