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

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
| Piece | What |
|---|---|
| Server | matrixconduit/matrix-conduit on port 6167, behind matrix.example.com |
| Client | vectorim/element-web at chat.example.com (or just use the Element apps) |
| Domain | user IDs are @name:example.com; two .well-known files on example.com point at matrix.example.com |
| Registration | a token, so only people you give it to can sign up |
| Apps | Element (iOS, Android, desktop), FluffyChat, Cinny |
| Check | federationtester.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:
- matrix.example.com → 127.0.0.1:6167, with client_max_body_size 20M.
- chat.example.com → 127.0.0.1:8070.
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.
