Overview
Matrix is a decentralized, end-to-end encrypted communications protocol. Unlike Signal or Discord, Matrix lets you run your own server. Your messages, member data, and encryption keys stay on hardware you control. No third party can read your conversations, shut down your server, or hand over your data.
This guide walks you through deploying a Matrix homeserver on a Raspberry Pi 5. The stack uses Synapse (the reference Matrix server implementation), Element Web (a browser-based chat client), and a Cloudflare tunnel to handle external access without opening ports on your network. The entire deployment runs in Docker containers, which means installation is clean, updates are simple, and the server is easy to back up or rebuild.
LFHI HQ runs the reference implementation on this same stack. When you complete this guide, your chapter will have a private, encrypted communications server that can optionally federate with other chapter servers across the country.
Prerequisites
Before you start, make sure you have the following:
- Raspberry Pi 5 – 4GB minimum, 8GB recommended. The Pi 4 works but is slower. Do not attempt this on a Pi 3 or Zero.
- microSD card – 32GB minimum, 64GB recommended. Use a card rated A2 for better random I/O performance.
- Power supply – Official Raspberry Pi 5 USB-C power supply (27W). Underpowered supplies cause instability.
- Ethernet cable – WiFi works but wired is significantly more reliable for a server that needs to stay online.
- Domain name – You need a domain you control. This can be your chapter’s existing domain or a new one. You will create two subdomains:
matrix.yourdomain.organdchat.yourdomain.org. - Cloudflare account – Free tier is sufficient. You will use Cloudflare for DNS and tunneling.
- Another computer – To SSH into the Pi and configure everything.
- About 2 hours – Most of that is waiting for downloads and reading configuration explanations.
Part 1: Pi Setup
Flash the Operating System
Download and install Raspberry Pi Imager on your computer. Insert the microSD card and open the Imager.
- Click Choose OS and select Raspberry Pi OS Lite (64-bit) under “Raspberry Pi OS (other).” Use the Lite version – you do not need a desktop environment on a server.
- Click Choose Storage and select your microSD card.
- Click the gear icon (or press Ctrl+Shift+X) to open advanced settings:
- Set hostname: Pick something descriptive, like
matrix-pior your chapter name. - Enable SSH: Select “Use password authentication” or set up public key authentication if you are comfortable with it.
- Set username and password: Create a user account. Do not use the default
piusername. - Configure WiFi: Only if you are not using Ethernet.
- Set locale: Your timezone and keyboard layout.
- Set hostname: Pick something descriptive, like
- Click Write and wait for the image to finish.
Insert the card into the Pi, connect Ethernet, and plug in power.
Find the Pi and Connect
After a minute or two, the Pi will be on your network. Find its IP address from your router’s admin page or by running:
ping matrix-pi.local
SSH into the Pi:
ssh youruser@matrix-pi.local
Update the System
Run a full system update:
sudo apt update && sudo apt upgrade -y
Reboot if the kernel was updated:
sudo reboot
Install Docker
Docker runs all three services (Synapse, Element Web, Cloudflare tunnel) in containers. Install Docker using the official convenience script:
curl -fsSL https://get.docker.com | sh
Add your user to the Docker group so you can run Docker commands without sudo:
sudo usermod -aG docker $USER
Log out and back in for the group change to take effect:
exit
SSH back in and verify Docker is working:
docker --version
docker compose version
Both commands should return version numbers. If docker compose is not found, install the compose plugin:
sudo apt install docker-compose-plugin
Configure the Firewall
Set up UFW (Uncomplicated Firewall) to block everything except SSH from your local network:
sudo apt install ufw -y
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 192.168.0.0/16 to any port 22 proto tcp
sudo ufw enable
Adjust the 192.168.0.0/16 range if your local network uses a different subnet (e.g., 10.0.0.0/8). This allows SSH only from your LAN. All other inbound traffic is blocked. The Cloudflare tunnel initiates outbound connections, so it works without any inbound port rules.
Verify the firewall is active:
sudo ufw status
Part 2: Domain and Cloudflare
All external traffic to your Matrix server flows through a Cloudflare tunnel. The tunnel is an outbound connection from your Pi to Cloudflare’s network, which means you do not need to open any ports on your router, configure port forwarding, or manage TLS certificates. Cloudflare handles all of it.
Add Your Domain to Cloudflare
If your domain is not already on Cloudflare:
- Log in to dash.cloudflare.com.
- Click Add a site and enter your domain.
- Select the Free plan.
- Cloudflare will provide two nameservers. Go to your domain registrar and update the nameservers to point to Cloudflare’s.
- Wait for DNS propagation (usually under an hour, sometimes up to 24 hours).
Create a Cloudflare Tunnel
- In the Cloudflare dashboard, click Zero Trust in the left sidebar.
- Navigate to Networks > Connectors.
- Under Cloudflare Tunnels, click Create a tunnel.
- Select Cloudflared as the connector type.
- Name your tunnel (e.g., “matrix-pi” or your chapter name).
- On the install page, Cloudflare will show you a command with a tunnel token. Copy the token – it is the long string that starts with
eyJ.... You will need it in the next step.
Do not add published application routes yet. You will do that after Synapse and Element are running.
Deploy Cloudflared on the Pi
Create the directory and compose file:
mkdir -p ~/docker/cloudflared
Create the Docker Compose file at ~/docker/cloudflared/docker-compose.yml:
services:
cloudflared:
container_name: cloudflared
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel run
environment:
- TUNNEL_TOKEN=your-tunnel-token-here
Replace your-tunnel-token-here with the token you copied from the Cloudflare dashboard.
Start the tunnel:
cd ~/docker/cloudflared && docker compose up -d
Verify the tunnel is connected:
docker logs cloudflared
You should see a line indicating the tunnel is connected and has registered connectors. In the Cloudflare dashboard, the tunnel status should show as Healthy.
Part 3: Deploy Synapse
Synapse is the Matrix homeserver. It handles user accounts, room state, message storage, federation with other servers, and the Matrix client-server API.
Generate the Configuration
Create the directory structure:
mkdir -p ~/docker/matrix/data
Generate the initial Synapse configuration:
docker run --rm \
-v ~/docker/matrix/data:/data \
-e SYNAPSE_SERVER_NAME=yourdomain.org \
-e SYNAPSE_REPORT_STATS=no \
matrixdotorg/synapse:latest generate
This creates homeserver.yaml and signing keys inside ~/docker/matrix/data/. The SYNAPSE_SERVER_NAME is your identity domain – this is the domain that appears in your Matrix IDs (@user:yourdomain.org). It is not the same as the subdomain where Synapse is accessible. Set it to your base domain.
Configure Synapse
Open the generated configuration file:
nano ~/docker/matrix/data/homeserver.yaml
The generated file contains secrets unique to your server: registration_shared_secret, macaroon_secret_key, form_secret, and signing_key_path. Do not delete or change those values. They were generated for your server and must stay.
Replace the rest of the configuration with the following hardened template. Keep your generated secrets and signing key path intact – the template marks where they go:
server_name: "yourdomain.org"
pid_file: /data/homeserver.pid
public_baseurl: "https://matrix.yourdomain.org"
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
bind_addresses: ['0.0.0.0']
resources:
- names: [client, federation]
compress: false
database:
name: sqlite3
args:
database: /data/homeserver.db
log_config: "/data/yourdomain.org.log.config"
# NOTE: The log_config filename above must match the file that was
# generated in your data directory. Replace "yourdomain.org" with the
# same domain you used in SYNAPSE_SERVER_NAME during the generate step.
# Check: ls ~/docker/matrix/data/*.log.config
media_store_path: /data/media_store
max_upload_size: 25M
# --- Keep your generated secrets below ---
registration_shared_secret: "YOUR_GENERATED_SECRET_HERE"
macaroon_secret_key: "YOUR_GENERATED_SECRET_HERE"
form_secret: "YOUR_GENERATED_SECRET_HERE"
signing_key_path: "/data/yourdomain.org.signing.key"
# --- End generated secrets ---
# Registration: closed by default, tokens required when enabled
enable_registration: false
registration_requires_token: true
# Federation
federation_enabled: true
serve_server_wellknown: true
# Telemetry
report_stats: false
# Trusted key servers
trusted_key_servers:
- server_name: "matrix.org"
# Rate limiting
rc_message:
per_second: 0.2
burst_count: 10
rc_registration:
per_second: 0.05
burst_count: 3
rc_login:
address:
per_second: 0.1
burst_count: 3
account:
per_second: 0.1
burst_count: 3
# URL previews
url_preview_enabled: true
url_preview_ip_range_blacklist:
- '127.0.0.0/8'
- '10.0.0.0/8'
- '172.16.0.0/12'
- '192.168.0.0/16'
- '100.64.0.0/10'
- '169.254.0.0/16'
- '::1/128'
- 'fe80::/10'
- 'fc00::/7'
# Suppress key share warnings
suppress_key_server_warning: true
A few notes on what these settings do:
server_name vs public_baseurl – server_name is your identity domain. It appears in Matrix IDs like @user:yourdomain.org. public_baseurl is where the server is actually reachable. These are intentionally different: your identity is yourdomain.org, but the server lives at matrix.yourdomain.org. The serve_server_wellknown setting tells Synapse to advertise the correct server location so federation and clients can find it.
enable_registration: false with registration_requires_token: true – This means nobody can create an account on your server without a registration token that you generate. You control exactly who joins.
Rate limiting – Limits message sending to about one message every five seconds with short bursts, and throttles login and registration attempts to slow brute force attacks.
url_preview_ip_range_blacklist – When a user shares a link and Synapse generates a preview, this prevents Synapse from making requests to internal network addresses. Without this, a malicious link could probe your LAN.
Save the file and exit (Ctrl+X, then Y, then Enter in nano).
Fix Permissions
Synapse runs as UID 991 inside the container. The data directory must be owned by this UID before the container starts, or Synapse will fail to write to its database and media store.
sudo chown -R 991:991 ~/docker/matrix/data
Create the Docker Compose File
Create ~/docker/matrix/docker-compose.yml:
services:
synapse:
container_name: synapse
image: matrixdotorg/synapse:latest
restart: unless-stopped
ports:
- "127.0.0.1:8008:8008"
volumes:
- ./data:/data
environment:
- TZ=America/New_York
Change the timezone (TZ) to match your location. Use the standard tz database format (e.g., America/Chicago, America/Denver, America/Los_Angeles).
The port binding 127.0.0.1:8008:8008 is critical. This binds Synapse to localhost only. It is not accessible from the network – only from the Pi itself and from the Cloudflare tunnel.
Start Synapse
cd ~/docker/matrix && docker compose up -d
Wait about 30 seconds for Synapse to initialize its database, then verify it is running:
curl http://localhost:8008/_matrix/client/versions
You should get a JSON response listing supported Matrix client versions. If you get a connection refused error, check the logs:
docker logs synapse --tail 50
The most common issue at this stage is a permissions problem on the data directory. If the logs mention permission errors, run the chown command from above again and restart the container:
sudo chown -R 991:991 ~/docker/matrix/data
docker restart synapse
Part 4: Deploy Element Web
Element Web is the browser-based Matrix client. It connects to your Synapse server and provides the chat interface your chapter members will use. Element is also available as a desktop and mobile app, and those apps can be pointed at your server. But hosting the web client means members can access chat from any device with a browser, with no installation required.
Create the Configuration
mkdir -p ~/docker/element-web
Create the Element Web configuration at ~/docker/element-web/config.json:
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.yourdomain.org",
"server_name": "yourdomain.org"
},
"m.identity_server": {
"base_url": "https://vector.im"
}
},
"disable_custom_urls": true,
"disable_guests": true,
"disable_3pid_login": true,
"brand": "Chapter Matrix",
"default_theme": "dark",
"room_directory": {
"servers": ["yourdomain.org"]
}
}
Replace yourdomain.org with your actual domain in both places. The base_url must match the public_baseurl you set in Synapse’s homeserver.yaml.
disable_custom_urls – Prevents users from pointing the client at a different server. Your Element instance is for your server only.
disable_guests – No anonymous browsing.
disable_3pid_login – Disables login via email or phone number. Members log in with their Matrix username and password.
Create the Docker Compose File
Create ~/docker/element-web/docker-compose.yml:
services:
element-web:
container_name: element-web
image: vectorim/element-web:latest
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
volumes:
- ./config.json:/app/config.json:ro
Like Synapse, Element Web binds to localhost only. The :ro flag mounts the config file as read-only inside the container.
Start Element Web
cd ~/docker/element-web && docker compose up -d
Verify it is running:
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080
This should return 200.
Part 5: Connect the Tunnel
With Synapse and Element Web both running on localhost, the final step is to route internet traffic to them through your Cloudflare tunnel. The localhost URLs you enter in the next step tell Cloudflare where to send traffic on the Pi itself. Users on the internet will access your server through https://matrix.yourdomain.org and https://chat.yourdomain.org – Cloudflare handles TLS and routes the traffic through the tunnel to the correct container.
Add Published Application Routes
Go back to the Cloudflare Zero Trust dashboard:
- Click Zero Trust in the left sidebar, then navigate to Networks > Connectors.
- Click on your tunnel name.
- Click the Published application routes tab.
- Click Add a published application route and fill in the first entry:
- Subdomain:
matrix - Domain: select
yourdomain.orgfrom the dropdown - Path: leave blank
- Type:
HTTP - URL:
localhost:8008
- Subdomain:
- Click Save.
- Go back and click Add a published application route again for the second entry:
- Subdomain:
chat - Domain: select
yourdomain.orgfrom the dropdown - Path: leave blank
- Type:
HTTP - URL:
localhost:8080
- Subdomain:
- Click Save.
Cloudflare automatically creates DNS records for both subdomains and provisions TLS certificates. No DNS configuration needed on your part.
Important: get the ports right. Synapse (the server) is localhost:8008. Element Web (the chat client) is localhost:8080. If you swap them, the Matrix API will not respond correctly. Double-check before saving.
If you see a DNS conflict error (“An A, AAAA, or CNAME record with that host already exists”) when saving a route, it means you already have a DNS record for that subdomain. Go to the main Cloudflare dashboard > your domain > DNS > Records, delete the conflicting record, then come back and save the published application route. The route will create the correct DNS record automatically.
Restart the Tunnel
After saving both routes, restart cloudflared so it picks up the new configuration:
docker restart cloudflared
Verify the new routes are loaded by checking the logs:
docker logs cloudflared 2>&1 | grep "Updated to new configuration"
You should see a line showing both hostnames in the ingress configuration. It will look something like:
Updated to new configuration config="{"ingress":[{"hostname":"matrix.yourdomain.org",...,"service":"http://localhost:8008"},{"hostname":"chat.yourdomain.org",...,"service":"http://localhost:8080"},{"service":"http_status:404"}],...}"
If you only see one hostname, the other route did not save. Go back to the dashboard and add it.
Test External Access
From your computer (not the Pi), test that the server is reachable:
curl https://matrix.yourdomain.org/_matrix/client/versions
You should get a JSON response listing supported Matrix client versions – the same response you saw when testing locally. Then test the well-known endpoint:
curl https://matrix.yourdomain.org/.well-known/matrix/server
This should return {"m.server":"yourdomain.org:443"}.
If either test fails with a 404 from nginx or a connection timeout, check:
- The tunnel is healthy in the Cloudflare dashboard (Zero Trust > Networks > Connectors – status should show Healthy).
- The published application routes are correct (matrix ->
localhost:8008, chat ->localhost:8080). - Synapse is running:
docker pson the Pi should show the synapse container. - The cloudflared logs show “Updated to new configuration” with both hostnames.
Open https://chat.yourdomain.org in a browser. You should see the Element Web login page. Do not try to log in yet – you have not created any user accounts.
Part 6: Create Admin, Space, and Rooms
Register the Admin Account
Synapse includes a command-line tool for creating user accounts. Create your admin account:
docker exec -it synapse register_new_matrix_user \
-c /data/homeserver.yaml \
-u admin -p 'YOUR_STRONG_PASSWORD' -a \
http://localhost:8008
Replace YOUR_STRONG_PASSWORD with a strong password. The -a flag makes this account a server administrator. Use a password manager to generate and store the password.
The tool will confirm the account was created. You now have @admin:yourdomain.org as the server administrator.
Log In
Open https://chat.yourdomain.org in your browser and log in with the admin account you just created. Element will prompt you to set up encryption keys and cross-signing. Complete this process – it is what ensures end-to-end encryption works correctly for your account.
Save the recovery key Element provides. Store it in your password manager or write it down and keep it secure. If you lose access to all your devices and do not have the recovery key, you will lose access to your encrypted message history.
Create Your Chapter Space
Matrix uses Spaces to organize rooms, similar to how Discord uses servers or Slack uses workspaces. A Space is a container that groups your chapter’s rooms together. When you invite a member to the Space, they can see and join all the rooms inside it. This is how you should organize your chapter – one Space with rooms inside it, not standalone rooms floating on their own.
Create the chapter Space:
- Click the + icon next to Spaces in the left sidebar.
- Select Create a space.
- Choose Private – only people you invite can find or join it.
- Name it with your chapter name (e.g., “LFHI Upson County” or “LFHI Nashville”).
- Optionally add a description (e.g., “Chapter coordination and training”).
- Click Create.
Create Rooms Inside the Space
Element will prompt you to add rooms to your new Space. Create the following rooms. All rooms should be set to Private (invite only, not visible in room directory).
#general – The main room for chapter-wide coordination, meeting scheduling, and general discussion. All members join this room.
#leadership – Restricted to chapter leads, role holders, and LFHI liaisons. Use this for planning, sensitive coordination, and admin discussions that do not need to go to the full chapter.
#announcements – Admin-only posting (set room permissions so only admins can send messages, everyone else is read-only). Use this for meeting reminders, schedule changes, and official chapter communications. Members can read but not post.
To create each room inside the Space:
- Click on your Space name in the left sidebar.
- Click the + button or Add room option within the Space.
- Select Create a new room.
- Enter the room name, set it to Private, and make sure Add to [Space Name] is checked.
- Click Create room.
After creating all three rooms, set the permissions on #announcements:
- Open the #announcements room.
- Click the room name at the top to open room settings.
- Go to Roles & Permissions.
- Under Default role, set it to a level that cannot send messages (e.g., set “Send messages” to Moderator or Admin only).
Room Permissions Overview
Matrix rooms have three default power levels:
- Default (0) – Regular members. Can read and send messages (unless restricted).
- Moderator (50) – Can kick/ban users, delete messages, and manage room settings.
- Admin (100) – Full control, including changing permissions and deleting the room.
For #general, leave default permissions as-is. All members can read and send messages.
For #leadership, leave default permissions as-is but only invite leadership members.
For #announcements, restrict “Send messages” to Moderator or Admin level. This makes it a broadcast channel – leadership posts, everyone reads.
You can add more rooms later as the chapter grows. Common additions: #training (scheduling and after-action), #comms (radio and technical), #medical (medical training coordination), #events (public outreach planning). Keep room count low to start. Two or three rooms are enough for most chapters in Phase 1. More rooms means more fragmented conversation.
Add Members
To create an account for a new member, SSH to your Pi and run:
docker exec -it synapse register_new_matrix_user \
-c /data/homeserver.yaml \
-u USERNAME -p 'TEMPORARY_PASSWORD' \
http://localhost:8008
Replace USERNAME with their chosen username and TEMPORARY_PASSWORD with a temporary password. When asked “Make admin?”, answer no.
Then tell the member:
- Go to
https://chat.yourdomain.orgin a browser. - Log in with the username and temporary password you gave them.
- Element will prompt them to set up encryption keys. Complete this step and save the recovery key somewhere safe.
- Click the avatar in the top-left corner, go to All Settings > Security & Privacy > Change Password and set a new password.
Once they are logged in, invite them to the chapter Space:
- Click on the Space name in the left sidebar.
- Click the Invite button (or the people icon).
- Enter their Matrix ID (
@username:yourdomain.org).
They will see all rooms inside the Space once they accept the invite.
Part 7: Security Checklist
Before considering your server operational, confirm every item on this list:
- Registration disabled.
enable_registration: falsein homeserver.yaml. Nobody can create accounts without a token. - Registration requires token.
registration_requires_token: truein homeserver.yaml. Even if registration were enabled, a token is still required. - Synapse listens on localhost only. The Docker port binding is
127.0.0.1:8008:8008. Rundocker port synapseto confirm – it should show127.0.0.1:8008. - Element Web listens on localhost only. The Docker port binding is
127.0.0.1:8080:80. Rundocker port element-webto confirm. - No ports exposed to the internet. All inbound traffic routes through the Cloudflare tunnel. No port forwarding on your router. Run
sudo ufw statusto verify the firewall is active and only SSH from your LAN is allowed. - URL preview blacklists internal networks. The
url_preview_ip_range_blacklistin homeserver.yaml covers RFC 1918 ranges, link-local, CGNAT, and IPv6 private ranges. - Rate limiting configured. Message, registration, and login rate limits are set in homeserver.yaml.
- No telemetry.
report_stats: falsein homeserver.yaml. - UFW firewall active. SSH allowed from LAN only, all other inbound traffic denied.
- Docker containers auto-restart. All three compose files use
restart: unless-stopped, so containers come back after a reboot or crash.
If any item is not in place, fix it before inviting members.
Part 8: Maintenance
Backups
The critical data lives in ~/docker/matrix/data/. This directory contains the SQLite database (all messages, room state, user accounts), media uploads, signing keys, and configuration. Back it up.
Weekly backup to an external drive or another machine:
# Stop Synapse to ensure database consistency
cd ~/docker/matrix && docker compose stop
# Create timestamped backup
mkdir -p ~/backups
tar czf ~/backups/matrix-$(date +%Y%m%d).tar.gz -C ~/docker/matrix data/
# Start Synapse again
cd ~/docker/matrix && docker compose start
Keep at least four weeks of backups. If you have an external USB drive or another machine on your network, copy backups there:
rsync -av ~/backups/ /mnt/usb/matrix-backups/
Stopping Synapse for the backup means a brief outage (usually under a minute). Schedule backups for a time when the chapter is unlikely to be active.
Updates
Update all three services by pulling the latest images and restarting:
cd ~/docker/matrix && docker compose pull && docker compose up -d
cd ~/docker/element-web && docker compose pull && docker compose up -d
cd ~/docker/cloudflared && docker compose pull && docker compose up -d
Check Synapse’s release notes before updating. Most updates are straightforward, but major version bumps occasionally require configuration changes or database migrations. Synapse handles SQLite migrations automatically on startup.
Update monthly or when security patches are released. After updating, verify the server is healthy:
curl http://localhost:8008/_matrix/client/versions
docker logs synapse --tail 20
Monitoring
Check that all containers are running:
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
All three containers (synapse, element-web, cloudflared) should show “Up” with their respective uptimes.
Check Synapse logs for errors:
docker logs synapse --tail 50
Check disk usage:
df -h /
du -sh ~/docker/matrix/data/
Synapse’s database and media store will grow over time. On a 64GB card with a chapter of 20-30 members, you have plenty of room for years of use. If disk usage becomes a concern, Synapse’s admin API supports purging old room history and unused media.
Registration Tokens
Keep a record of tokens you generate, who they are for, and whether they have been used. The admin API shows all tokens and their usage status. Periodically clean up expired or unused tokens:
curl http://localhost:8008/_synapse/admin/v1/registration_tokens \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Space and Room Management
As admin, you can perform these common tasks through Element Web:
- Invite members to the Space – Click the Space name, click Invite, enter their Matrix ID (@user:yourdomain.org). They will see all rooms inside the Space.
- Add new rooms to the Space – Click the Space name, click Add Room, create or link an existing room. New rooms inherit the Space’s visibility.
- Set room permissions – Room settings let you control who can send messages, invite members, change topics, and manage the room.
- Remove members – Right-click a user in the member list and select “Remove from room” or “Ban.” Note: removing someone from the Space does not automatically remove them from rooms inside it. You need to remove them from each room individually. To fully remove a member, kick or ban them from each room they are in, then remove them from the Space.
- Create sub-spaces – If your chapter grows large enough, you can create sub-spaces within the main Space (e.g., a “Training” sub-space with rooms for each training area). Most chapters will not need this.
Appendix A: Federation
Federation is what makes Matrix decentralized. When your server federates with another server, members on your server can join rooms on the other server, send direct messages to its users, and participate in shared spaces. The conversation data is replicated across both servers.
Your server federates by default. The serve_server_wellknown: true setting in homeserver.yaml tells Synapse to serve the .well-known/matrix/server endpoint, which tells other servers where to find yours. Since your traffic goes through a Cloudflare tunnel, federation works without opening any additional ports.
Testing Federation
After your server has been running for a few minutes, test federation using the Matrix Federation Tester:
- Open federationtester.matrix.org in your browser.
- Enter your
server_name(yourdomain.org, not the matrix. subdomain). - The tester will check .well-known delegation, TLS, and server connectivity.
- All checks should pass green.
If federation tests fail, verify that serve_server_wellknown: true is set in homeserver.yaml and that your Cloudflare tunnel is routing traffic to Synapse on port 8008. The .well-known endpoint is served by Synapse itself, so no separate configuration is needed.
Federating with Other Chapters
Once two chapter servers are both federated, members can interact across servers. To join a room on another chapter’s server:
- In Element Web, click + next to Rooms.
- Select Join a room.
- Enter the room address: #roomname:otherchapter.org.
The chapter lead of the other server will need to set the room to allow federation (it is allowed by default for invite-only rooms if you are invited).
Disabling Federation
If you want a fully isolated server with no outside connections, set the following in homeserver.yaml:
federation_domain_whitelist: []
This prevents your server from federating with any other server. Members can only communicate with other members on the same server. You can also selectively federate by listing specific domains in the whitelist.
Restart Synapse after changing federation settings:
cd ~/docker/matrix && docker compose restart
Appendix B: PostgreSQL Migration
SQLite is the right choice for chapter-size deployments. It requires no additional setup, no separate database server, and performs well for groups of up to about 50 active users. If your chapter grows beyond that or you notice performance degradation (slow room loads, delayed message delivery), consider migrating to PostgreSQL.
The migration path is:
- Add a PostgreSQL container to your Docker Compose setup.
- Create a
synapsedatabase and user in PostgreSQL. - Run Synapse’s built-in
synapse_port_dbscript to migrate data from SQLite to PostgreSQL. - Update homeserver.yaml to point to the PostgreSQL database.
- Restart Synapse.
The full process is documented in the Synapse database migration guide. The migration script handles schema conversion automatically. Plan for downtime during the migration – the script needs exclusive access to both databases.
Do not start with PostgreSQL unless you have a specific reason. SQLite is simpler to back up (it is a single file), simpler to restore, and performs well within the expected scale of a chapter server.
Appendix C: Troubleshooting
Synapse Will Not Start
Check the logs first:
docker logs synapse --tail 50
Permission errors. The data directory must be owned by UID 991. Fix with:
sudo chown -R 991:991 ~/docker/matrix/data
docker restart synapse
YAML syntax errors. If you edited homeserver.yaml and introduced a syntax error, the logs will show the exact line. Common mistakes: missing colons, incorrect indentation, tabs instead of spaces (YAML requires spaces).
Port conflicts. If another service is already using port 8008, Synapse will fail to bind. Check with ss -tlnp | grep 8008. Stop the conflicting service or change Synapse’s port in both homeserver.yaml and docker-compose.yml.
Cannot Reach the Server Externally
If curl https://matrix.yourdomain.org/_matrix/client/versions fails from outside the Pi:
- Check the tunnel. Run
docker logs cloudflaredon the Pi. Look for “Registered tunnel connection” lines and “Updated to new configuration” showing both hostnames. If the token is wrong or expired, the logs will show authentication errors. - Check DNS. Run
nslookup matrix.yourdomain.orgfrom your computer. It should resolve to a Cloudflare IP address. If it does not resolve, the published application route was not saved correctly or the DNS record was not auto-created. - Check the published application routes. In Cloudflare Zero Trust > Networks > Connectors > your tunnel > Published application routes, verify both routes exist and point to
localhost:8008(matrix) andlocalhost:8080(chat). A common mistake is swapping the ports – Synapse is 8008, Element Web is 8080. - Check Synapse is running.
docker psshould show the synapse container as running.curl http://localhost:8008/_matrix/client/versionsfrom the Pi itself should return JSON. - Restart cloudflared. After adding or editing published application routes, always restart the tunnel:
docker restart cloudflared. Then check the logs to confirm the new configuration was loaded.
Element Web Shows “Connection Error”
This means Element loaded successfully but cannot reach Synapse.
- Check config.json. The
base_urlin~/docker/element-web/config.jsonmust match your Synapsepublic_baseurlexactly, including the scheme (https://) and subdomain (matrix.yourdomain.org). A mismatch here is the most common cause. - Check Synapse is running. If Synapse crashed or is restarting, Element has nothing to connect to. Run
docker psanddocker logs synapse --tail 20. - Check CORS. Cloudflare tunnels handle CORS headers automatically. If you have added any custom headers in Cloudflare, ensure they are not interfering.
Federation Test Fails
If federationtester.matrix.org reports errors:
- Check serve_server_wellknown. Must be
truein homeserver.yaml. Synapse serves the.well-known/matrix/serverendpoint, which tells other servers thatyourdomain.orgdelegates tomatrix.yourdomain.org:443. - Test .well-known directly. Run
curl https://yourdomain.org/.well-known/matrix/serverfrom your computer. If it does not return JSON with your server’s address, the well-known delegation is not working. - DNS issues. Both
yourdomain.organdmatrix.yourdomain.orgmust resolve to Cloudflare IPs. The base domain needs a proxied DNS record (usually an A or CNAME for the root domain pointing to Cloudflare).
If your Cloudflare tunnel only has a public hostname for matrix.yourdomain.org and not the bare yourdomain.org, federation may fail because other servers try to fetch .well-known from the bare domain first. You can solve this by adding a third public hostname in the tunnel that routes yourdomain.org to localhost:8008 as well, or by creating a Cloudflare redirect rule that sends yourdomain.org/.well-known/* to matrix.yourdomain.org/.well-known/*.
Registration Token Not Working
If a member enters a registration token and gets an error:
- Verify registration_requires_token is true in homeserver.yaml. The token system requires this setting.
- Check the token is valid. List all tokens with the admin API and confirm the token has not already been used or expired.
- Check the member is entering it correctly. Tokens are case-sensitive. Send the token via Signal where the member can copy-paste it.
High Memory Usage
Synapse uses more memory during initial federation. When your server first connects to a federated room, it downloads the room’s state history. For large public rooms this can spike memory usage temporarily. On a Pi 5 with 4GB or 8GB of RAM, this is rarely a problem for chapter-size deployments with private rooms.
If you are concerned about memory:
free -h
docker stats --no-stream
Synapse typically uses 200-400MB of RAM for a small server. If it is consistently above 1GB with only chapter rooms, check for runaway federation with large public rooms and consider leaving those rooms.