MATRIX HAS YOU FIELD MANUAL

SELF-HOSTING / DOCKER COMPOSE / EDITION 2026.10

Run your own
Matrix.

Messages, federation and calls. A practical setup guide for a server you own—with the Docker files, configs and sharp edges included.

12 files · fresh secrets on your server · no account needed

Linux · public IPv47 services · digest-pinned images4.75 GiB · combined memory capsWhat has been checked

1. Prepare the server and DNS

Use a Linux server with a static public IPv4 assigned directly to it, Docker Engine, the Compose plugin (docker compose) and Python 3.9+. The source deployment is Linux/amd64. This guide uses Linux host networking for Coturn; Docker Desktop, rootless Docker and a server behind router/cloud NAT need networking changes and are outside this recipe.

Ports 80, 443 and 8448 must be available for this stack's Traefik. If another reverse proxy already owns them, integrate the routes into that proxy before starting. There is no Tailscale dependency. Docker installation instructions: Docker Engine on Ubuntu.

For DOMAIN=example.com, create these DNS A records, all pointing to your server's public IPv4:

Name Purpose
matrix.example.com Synapse API, federation, client discovery
chat.example.com Element Web
rtc.example.com LiveKit signaling and MatrixRTC authorization
turn.example.com Coturn

Use direct DNS records, without a CDN HTTP proxy in front of TURN or LiveKit's media ports. Publish AAAA records only after configuring and testing IPv6.

Choose your Matrix identity now. This recipe uses server_name: matrix.example.com, producing IDs such as @alice:matrix.example.com. Treat that name as permanent once the server is in use. If you want @alice:example.com, arrange root-domain delegation and adjust the server-name references before initialization; this recipe deliberately uses the simpler subdomain layout. Synapse server names

Allow the following incoming traffic in the provider firewall and host firewall:

Port Protocol Purpose
80 TCP Let's Encrypt HTTP-01 and HTTPS redirect
443 TCP Matrix, Element and RTC HTTPS/WebSocket
8448 TCP Federation fallback/direct access
3478 TCP + UDP Coturn client connections
61000–65535 UDP Coturn relay allocations
7881 TCP LiveKit ICE/TCP fallback
7882 UDP LiveKit ICE/UDP mux

Keep SSH access available. The stack does not expose PostgreSQL or LiveKit's internal API port. Synapse's port 8008 binds only to 127.0.0.1 for local admin access. Docker-published ports can bypass ordinary UFW rules; account for Docker's firewall behavior rather than relying on UFW alone. Docker firewall behavior

Coturn's relay pool assumes the host's ephemeral port range ends below 61000. Check cat /proc/sys/net/ipv4/ip_local_port_range if your host uses a custom range.

2. Generate this server's config

Work as your normal Linux user with Docker access. Do not run configure.py with sudo; Synapse will run with this user's UID/GID so it can write its data.

cd matrix-setup
cp settings.env.example settings.env
nano settings.env
python3 configure.py
docker compose config --quiet

Set your real DOMAIN, PUBLIC_IPV4 and ACME_EMAIL in settings.env. Use plain KEY=value lines without quotes. The script generates six independent random secrets, copies matching values wherever needed, and creates:

Path Contents
.env Compose variables, credentials and Synapse UID/GID
generated/homeserver.yaml Synapse database, identity, TURN and MatrixRTC configuration
generated/log.config Synapse console logging
generated/element.json Element homeserver selection
generated/livekit.yaml LiveKit IP, media ports and API key
generated/turnserver.conf Coturn authentication, listeners and relay policy
data/ Persistent PostgreSQL, Synapse and Traefik state

configure.py starts no containers and refuses to overwrite .env, generated/ or data/. It is a one-time initializer, not a reconfiguration or secret rotation tool. After initialization, edit generated files deliberately and keep shared values in sync. Editing settings.env alone does not reconfigure a server.

The script protects .env and the generated/ directory with private host permissions. Individual config files are readable by the differing image users through their bind mounts. These paths, settings.env, data/ and backups/ are ignored by the included .gitignore. They contain private installation state and should not be included when sharing this guide.

3. Generate the signing key and start

docker compose pull
docker compose run --rm --no-deps --entrypoint python synapse \
  -m synapse.app.homeserver --generate-keys -c /data/homeserver.yaml
docker compose up -d
docker compose ps
docker compose logs --tail=100 traefik postgres synapse livekit lk-jwt-service coturn

The key command creates data/synapse/server.signing.key without starting the database. Preserve that key for the lifetime of the server. The image's generation behavior is documented in the Synapse Docker instructions.

Traefik obtains certificates for Matrix, Element and RTC on port 80 and serves HTTPS on 443. Synapse serves /.well-known/matrix/client itself; its additional RTC content points to the JWT service. serve_server_wellknown: true advertises federation on 443, while 8448 is also served with the same certificate. Synapse delegation

Resource caps are inherited from the source stack and total about 4.75 GiB across these services. They are limits, not reserved host memory. Leave RAM for the OS and adjust limits to the number of users and media activity you observe.

4. Create accounts

Public registration is disabled. Create the first account interactively:

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

Enter a local username and password; answer yes to the admin prompt for the first operator account. Repeat with no for ordinary accounts. Keeping the password interactive avoids putting it in shell history. Account creation

Open https://chat.example.com using your actual domain and sign in. Other compatible clients can use https://matrix.example.com as the homeserver. This bundle uses Synapse's local authentication; it does not include Matrix Authentication Service, SMTP/password-reset mail or an identity server.

The public reverse-proxy route blocks /_synapse/admin. For an admin tool, forward port 8008 over SSH to the server's loopback address, or run the required admin operation locally. No public admin dashboard is included.

5. Verify the new deployment

Substitute your chosen domain in these commands, and run the HTTPS checks from a machine outside the server:

curl -fsS https://matrix.example.com/_matrix/client/versions
curl -fsS https://matrix.example.com/.well-known/matrix/client
curl -fsS https://matrix.example.com/.well-known/matrix/server
curl -fsS https://matrix.example.com:8448/_matrix/federation/v1/version
curl -fsS https://rtc.example.com/livekit/jwt/healthz

Check that discovery returns your own homeserver and https://rtc.example.com/livekit/jwt. The modern authenticated MatrixRTC transport registry and the older well-known announcement are both configured, following the Element Call prerequisites. The in-app calling setup does not require a separate standalone Element Call web container.

Use the Matrix federation tester for your server name, then exchange messages with an account on another homeserver. An API returning 200 is not a federation or call test.

For calling, use two accounts on different external networks, for example home broadband and mobile data. Check both audio directions, video, joining/leaving and an encrypted call. In Chromium, chrome://webrtc-internals can show the selected ICE candidate pair. LiveKit also logs the selected transport:

docker compose logs --since=10m livekit

A normal direct media path should select your public IP on UDP 7882. TCP 7881 can work as fallback, so a successful call alone does not establish that UDP is reachable. LiveKit port reference

Coturn serves the TURN URLs advertised by Synapse for clients/call modes that use them. It is not automatically a TURN relay for LiveKit just because it is listed in Synapse. This recipe has no TURN/TLS on 443 or embedded LiveKit TURN; networks that block the configured media paths need a separately designed relay setup. Do not change the UDP mapping to 7881: LiveKit listens on 7882.

Files to keep and maintenance

Back up .env, settings.env, generated/, Synapse's signing key and media, Traefik ACME state, and PostgreSQL. A database dump alone does not preserve the whole server. For a small server, this creates a database/media-consistent backup by stopping Synapse while PostgreSQL remains available:

umask 077
backup_dir="backups/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$backup_dir"
docker compose stop synapse
docker compose exec -T postgres pg_dump -U synapse -d synapse -Fc \
  > "$backup_dir/synapse.dump"
# Continue only if pg_dump succeeded:
sudo tar -czf "$backup_dir/config-media.tar.gz" \
  .env settings.env generated data/synapse data/traefik
docker compose start synapse

If a backup step fails, restart Synapse and treat that backup as incomplete. Copy the dump and archive to private off-server storage. Test restoration on an isolated host: restore the files with their permissions and the same server identity, start an empty PostgreSQL 15 service, load the dump with pg_restore, then start Synapse. Do not run configure.py over restored data or allow two servers with the same identity to federate at once.

All seven image references are digest-pinned to the source snapshot, including ones whose tag text says latest. docker compose pull therefore does not silently upgrade them. images.json records their origins. Review upstream releases, back up first, and update both tag and digest deliberately. Do not change the PostgreSQL major version without a database migration plan.

After changing generated configuration, recreate the affected service so that individual file bind mounts also pick up atomically replaced files:

docker compose config --quiet
docker compose up -d --force-recreate --no-deps synapse

Replace synapse with the service you changed; LiveKit recreation interrupts active calls. Never rerun the initializer to change passwords on an existing database. Keep .env and generated/homeserver.yaml aligned when changing the PostgreSQL password, and both LiveKit/JWT or Coturn/Synapse copies when changing their shared secrets.

Differences from the source deployment

  • The relevant services are combined into one Compose project with relative paths and project-owned networks. Existing VPS Compose files are untouched.
  • Tailscale addresses, the source domain/IP, the Traefik dashboard and unrelated workloads are removed. Local admin access uses loopback port 8008.
  • Fresh secrets replace all source credentials. Synapse runs as the initializing user; filenames do not contain a particular domain.
  • Logging is bounded to three 10 MB Docker log files per service.
  • The unused Redis service is omitted: the captured single-node LiveKit config does not reference Redis. Distributed LiveKit needs separate planning.
  • Current MatrixRTC transport discovery/rate settings supplement the source's well-known announcement. LiveKit room auto-creation is disabled so the JWT service's local-homeserver allowlist controls creation, as required by the JWT service v0.4.1 instructions.

This is a setup guide, not a backup of the original server. See VALIDATION.md for what was actually checked and what still needs verification on your own domain and server.

Share a clean copy

From this folder, build an archive containing only the guide and templates:

tar -czf ../matrix-setup-guide.tar.gz \
  README.md VALIDATION.md compose.yml configure.py settings.env.example \
  .gitignore images.json templates

This explicit file list excludes installation credentials, runtime data and backups even after you have used the guide yourself.

Inside the bundle

The complete setup lives in a folder you can keep, inspect and change. No generated credentials or runtime data are included.

Archive SHA-256dfdb0a203861f119a6b21d503041a792a111374742a1b3cd48b9dbc06dca085f