Deploying a Matrix Synapse Server with Docker and Traefik

Table of Contents 📑
- Changelog
- 1. Prerequisites
- 2. Directory Structure
- 3. Download the Ansible Playbook
- 4. Initial Configuration
- 5. Install Ansible and Dependencies
- 6. Configure Ansible Hosts
- 7. Adjust Traefik for Federation
- 8. Run the Matrix Installation
- 9. Start the Matrix Services
- 10. Verify the Installation
- 11. Create Your First User
- 12. Log In with Element
- 13. Accessing the Ketesa Admin UI
- 14. Maintenance
- 15. Troubleshooting
- 15.1 Federation test fails
- 15.2 .well-known is invalid or missing
- 15.3 Traefik/CrowdSec middleware not applied
- 15.4 ACME/TLS issues
- 15.5 Docker network not found
- 15.6 Permission problems in inventory/host_vars
- 15.7 Ansible/venv not active
- 15.8 CrowdSec bouncer not registered
- 15.9 Element Call: MISSING_MATRIX_RTC_TRANSPORT
- Conclusion
This guide will walk you through deploying a powerful, federated Matrix Synapse homeserver. We will use the popular matrix-docker-ansible-deploy playbook, which simplifies the setup of Synapse and its various components, including bridges for other chat platforms.
This setup is designed to integrate seamlessly with an existing Traefik v3 reverse proxy and a root-domain Nginx web server, making it a perfect addition to a modern, container-based infrastructure.
Changelog
| Date | Change |
|---|---|
| 2026-07-01 | Migration Notes: Added homeserver identity, bridge-state, MatrixRTC discovery, and media-retention guidance. |
| 2026-06-12 | Playbook Compatibility: Refreshed the guide for current Ketesa, Telegram bridgev2, migration validation, Docker collection requirements, and a safer baseline without Matrix RTC. |
| 2026-02-20 | Configuration Update: Updated matrix_coturn_enabled to coturn_enabled, removed Sliding Sync, and added PostgreSQL upgrade instructions. |
| 2025-09-17 | Initial Version: Guide created, focusing on Traefik v3 integration, Ansible deployment, and enabling various bridges and features like Sliding Sync. |
1. Prerequisites
- Traefik v3 and CrowdSec with Docker Compose: A Modern Security Stack: This is the foundation for our reverse proxy and security.
- Deploying a Secure Nginx Website with Traefik and Docker Compose: This step is crucial as it establishes a web server on your root domain and correctly configures the
.well-known/matrixdelegation required for federation.
| ⚠️ HARD REQUIREMENT |
The following steps will not work correctly without the Traefik and Nginx stacks running as described in the prerequisite guides. |
| ℹ️ ROOT DOMAIN REMAINS FREE FOR YOUR WEBSITE |
Synapse runs on a dedicated subdomain (e.g., |
Architecture (simplified)
Users / Browsers / Homeservers
│
▼
your-domain.com ──> [Nginx](../nginx_webserver/) on root domain
│ │
│ ├─ Serves your website (/, /assets, ...)
│ └─ Serves .well-known/matrix/{server,client}
│ │
│ ▼
└────────────────────────── delegates to ──> matrix.your-domain.com
(Synapse via Traefik)
│
▼
Traefik (websecure, synapse)
│
▼
SynapseYou will also need:
git,jq, andpwgenfor version control, JSON parsing, and password generation.- Python 3 and
pipinstalled on your server. sudoor root access.
Install the required tools:
sudo apt update && sudo apt -y install git jq pwgen python3-pip python3-venv make2. Directory Structure
First, we’ll create a dedicated directory for the Matrix playbook configuration and files.
sudo mkdir -p /opt/containers/matrix3. Download the Ansible Playbook
Next, clone the official matrix-docker-ansible-deploy repository from GitHub into the directory you just created.
sudo git clone https://github.com/spantaleev/matrix-docker-ansible-deploy.git /opt/containers/matrix4. Initial Configuration
The playbook is configured using Ansible variables. We’ll start by creating a configuration directory based on your Matrix server’s hostname and copying the example vars.yml file.
| ℹ️ HOSTNAME CONVENTION |
The playbook expects the server to be available at |
cd /opt/containers/matrix
# Replace 'matrix.your-domain.com' with your actual server name
sudo mkdir -p inventory/host_vars/matrix.your-domain.com
sudo cp examples/vars.yml inventory/host_vars/matrix.your-domain.com/4.1. Generate Secret Keys
The configuration requires two strong secret keys. You can generate these using pwgen or openssl.
# Generate two strong keys and save them for the next step
pwgen -s 64 1
pwgen -s 64 14.2. Customize vars.yml
Now, open the vars.yml file and customize it for your environment. This is the most critical step.
sudo nano inventory/host_vars/matrix.your-domain.com/vars.ymlBelow is a complete configuration based on a feature-rich setup. Adjust the values to match your domain, secrets, and desired features.
# Acknowledge the playbook breaking changes reviewed for this guide.
matrix_playbook_migration_validated_version: v2026.05.18.0
# The bare domain name which represents your Matrix identity.
matrix_domain: your-domain.com
matrix_homeserver_implementation: synapse
matrix_homeserver_generic_secret_key: 'PASTE-YOUR-GENERIC-SECRET-KEY-HERE'
# Use our own Traefik (externally managed)
matrix_playbook_reverse_proxy_type: other-traefik-container
traefik_certs_dumper_enabled: false
postgres_connection_password: 'PASTE-YOUR-POSTGRES-PASSWORD-HERE'
devture_systemd_docker_base_docker_service_name: "docker"
matrix_playbook_docker_installation_enabled: false
# We delegate .well-known from the root domain via Nginx
matrix_static_files_enabled: false
matrix_static_files_container_labels_traefik_enabled: false
coturn_enabled: false
# Traefik integration
matrix_playbook_reverse_proxy_container_network: proxy
matrix_playbook_reverse_proxy_hostname: traefik
traefik_entrypoint_primary: websecure
traefik_certResolver_primary: tls_resolver
matrix_federation_traefik_entrypoint_name: synapse
# --- Core services ---
matrix_synapse_container_labels_traefik_docker_network: proxy
matrix_client_element_container_labels_traefik_docker_network: proxy
matrix_client_element_hostname: "{{ matrix_server_fqn_matrix }}"
matrix_client_element_path_prefix: /element
matrix_admin: '@your-username:your-domain.com'
matrix_ketesa_enabled: true
matrix_ketesa_container_http_host_bind_port: ""
matrix_ketesa_container_labels_traefik_docker_network: proxy
# E-Mail relay (optional)
exim_relay_sender_address: "matrix@your-domain.com"
exim_relay_relay_use: true
exim_relay_relay_host_name: "mail.your-domain.com"
exim_relay_relay_host_port: 587
exim_relay_relay_auth: true
exim_relay_relay_auth_username: "user@your-domain.com"
exim_relay_relay_auth_password: "YOUR-EMAIL-PASSWORD"
matrix_appservice_double_puppet_enabled: true
# --- Bridges ---
# The playbook can install and configure various bridges to connect your Matrix homeserver
# to other chat networks like Telegram, Discord, Signal, etc. When enabling a bridge,
# it is crucial to provide the `homeserver` configuration block to ensure it communicates
# correctly with Synapse via Traefik. For more details on available bridges and their
# specific configurations, refer to the official documentation.
# Telegram – Correct EntryPoints + specify Homeserver
matrix_mautrix_telegram_enabled: true
matrix_mautrix_telegram_api_id: 12345678
matrix_mautrix_telegram_api_hash: 'your_telegram_api_hash'
matrix_mautrix_telegram_configuration_extension_yaml: |
homeserver:
address: "https://matrix.your-domain.com"
domain: "your-domain.com"
bridge:
permissions:
'{{ matrix_admin }}': admin
# Meta Messenger
matrix_mautrix_meta_messenger_enabled: true
matrix_mautrix_meta_messenger_configuration_extension_yaml: |
homeserver:
address: "https://matrix.your-domain.com"
domain: "your-domain.com"
bridge:
permissions:
'*': relay
'your-domain.com': admin
'{{ matrix_admin }}': admin
# Discord – Correct Avatar-Proxy & general EntryPoints
matrix_mautrix_discord_enabled: true
matrix_mautrix_discord_configuration_extension_yaml: |
homeserver:
address: "https://matrix.your-domain.com"
domain: "your-domain.com"| ℹ️ TRAEFIK MIDDLEWARES |
The playbook generates Traefik router labels for each Matrix component. Do not add custom middleware variables such as |
| ℹ️ ELEMENT CALL / MATRIX RTC |
Element Call and the Matrix RTC stack are deliberately not enabled in the baseline configuration above. They need additional firewall ports ( |
5. Install Ansible and Dependencies
This playbook uses Ansible to automate the setup. Choose the installation method that fits your distribution policy.
For Debian 12 (system pip with –break-system-packages)
# Ensure pip is available
sudo apt install -y python3-pip
# Install Ansible and required Python packages system-wide
sudo pip3 install --break-system-packages ansible docker passlibFor Other Linux Distributions (recommended: Python virtual environment)
# Create and activate a dedicated virtual environment
python3 -m venv ~/.venvs/matrix-ansible
source ~/.venvs/matrix-ansible/bin/activate
# Upgrade pip and install required packages in the venv
pip install --upgrade pip
pip install ansible docker passlib| ℹ️ VIRTUALENV REMINDER |
If you use the virtual environment, remember to reactivate it in new shell sessions with: |
| ℹ️ USE ONE ANSIBLE ENVIRONMENT |
Run |
Next, install the Ansible roles required by the playbook:
cd /opt/containers/matrix
sudo make rolesInstall the Docker collection used by several playbook roles:
sudo ansible-galaxy collection install community.docker --upgradeYou can verify that Ansible can resolve the Docker image module:
sudo ansible-doc community.docker.docker_image_pull6. Configure Ansible Hosts
Now, we need to tell Ansible which server to configure. Since we are running it locally, the setup is simple.
# Copy the example hosts file
sudo cp examples/hosts inventory/hosts
# Edit the file
sudo nano inventory/hostsModify the file to look like this. The ansible_connection=local tells Ansible to run all commands on the machine it’s currently on, instead of connecting to a remote server via SSH. Because of this, ansible_host=127.0.0.1 (localhost) is the correct value. Parameters like ansible_ssh_user=root are ignored in this mode, as the playbook runs with the permissions of the user executing it (in our case, root via sudo).
[matrix_servers]
matrix.your-domain.com ansible_host=127.0.0.1 ansible_connection=local7. Adjust Traefik for Federation
For Matrix federation to work, other servers need to connect to yours on port 8448. We must expose this port in our main Traefik stack and create a dedicated entrypoint for it.
Go to your Traefik stack’s directory (e.g., /opt/containers/traefik-stack).
7.1. Expose Federation Port
Edit your main docker-compose.yml:
sudo nano docker-compose.ymlAdd port 8448 to the ports section of the traefik service:
services:
traefik:
# ... other settings
ports:
- "80:80"
- "443:443"
- "8448:8448" # Add this line for Matrix federation
# ... rest of the settings7.2. Create Federation and Internal Entrypoints
Edit your static Traefik configuration (traefik/config/traefik.yml):
sudo nano traefik/config/traefik.ymlAdd a new synapse entrypoint for federation and a matrix-internal-matrix-client-api entrypoint for internal communication.
For recent playbook versions, it is recommended to also define the internal matrix-internal-matrix-client-api entrypoint on port 8008. This port is not published to the host and serves as an internal C2S route for bridges and add-ons, preventing errors in the Traefik dashboard.
entryPoints:
web:
address: ":80"
# ...
websecure:
address: ":443"
synapse: # For federation
address: ":8448"
matrix-internal-matrix-client-api: # For internal C2S communication
address: ":8008"7.3. Restart Traefik
Apply the changes by restarting your Traefik stack:
cd /opt/containers/traefik-stack
docker compose up -d8. Run the Matrix Installation
With all the configuration in place, we can now run the Ansible playbook to set up and install everything.
cd /opt/containers/matrix
sudo ansible-playbook -i inventory/hosts setup.yml --tags=install-allThis command will download all necessary Docker images, generate configuration files, and prepare the services. It may take several minutes. The install-all tag is the recommended path for a new server when you are not importing old Matrix data.
9. Start the Matrix Services
Once the setup is complete, start all the Matrix containers:
sudo ansible-playbook -i inventory/hosts setup.yml --tags=ensure-matrix-users-created,startAfter a few moments, all the services you enabled should be running. The ensure-matrix-users-created tag creates the internal service users required by bots and bridges. You can verify the running containers with docker ps.
10. Verify the Installation
After starting the services, it’s crucial to verify that all components are running and communicating correctly.
10.1. Check Running Containers
First, ensure all enabled services are running. The exact names will vary based on your matrix_domain.
docker ps --format '{{.Names}}' | grep -E 'synapse|element|ketesa|mautrix'10.2. Verify .well-known Delegation (CLI)
Check if your Nginx server is correctly serving the delegation files for your root domain.
# Replace 'your-domain.com' with your actual domain
curl -s https://your-domain.com/.well-known/matrix/server | jq
curl -s https://your-domain.com/.well-known/matrix/client | jqFor this guide, the expected federation delegation uses port 8448:
{
"m.server": "matrix.your-domain.com:8448"
}The client delegation should point clients to the Matrix HTTPS endpoint:
{
"m.homeserver": {
"base_url": "https://matrix.your-domain.com"
}
}10.3. Verify Federation Endpoint (CLI)
Test if the Synapse federation port (8448) is correctly exposed through Traefik.
# Replace 'matrix.your-domain.com' with your actual matrix subdomain
curl -sS https://matrix.your-domain.com:8448/_matrix/federation/v1/version | jqThis should return a JSON object with server information, confirming the federation endpoint is reachable.
10.4. Verify CrowdSec Bouncer Registration
Check if the CrowdSec bouncer for Traefik has successfully registered with the CrowdSec agent.
cd /opt/containers/traefik-stack
docker compose exec crowdsec cscli bouncers listYou should see the Traefik bouncer in the list with a valid status.
10.5. Web-based Federation Test
Finally, use the official Federation Tester for a comprehensive check.
- Go to: https://federationtester.matrix.org/
- Enter your server name (e.g.,
your-domain.com) and run the test.
If everything is configured correctly, you should see a success message.
11. Create Your First User
Use the playbook to register your administrator user.
| ⚠️ PASSWORD SECURITY |
To avoid leaving your password in your shell’s history, omit the
|
cd /opt/containers/matrix
sudo ansible-playbook -i inventory/hosts setup.yml --tags=register-user -e 'username=your-username' -e 'admin=yes'You can re-run this command with admin=no to create non-admin users.
12. Log In with Element
Your homeserver is now ready! You can access the Element web client by navigating to https://matrix.your-domain.com/element.
- Click “Sign In”.
- Your homeserver should be pre-filled. If not, edit it to show your server’s address (
matrix.your-domain.com). - Log in with the username and password you just created.
13. Accessing the Ketesa Admin UI
Because we set matrix_ketesa_enabled: true in our vars.yml configuration, a powerful web-based administration interface for Synapse is automatically deployed.
You can access it at: https://matrix.your-domain.com/synapse-admin
By default, the playbook exposes Ketesa at the /synapse-admin path. If you prefer a Ketesa-branded path, add matrix_ketesa_path_prefix: /ketesa to vars.yml and use https://matrix.your-domain.com/ketesa instead.
Log in with your Matrix administrator account. From here, you can:
- Manage users (deactivate, make admin, etc.).
- View server statistics and metrics.
- Explore rooms and manage their settings.
This interface is the primary tool for the day-to-day administration of your homeserver.
14. Maintenance
Routine Updates
To update your Matrix server and its components to the latest version:
cd /opt/containers/matrix
# Pull the latest changes from the git repository.
sudo git pull
# Update the playbook roles.
sudo make roles
# Keep Ansible collections current.
sudo ansible-galaxy collection install community.docker --upgrade
# It's a good practice to review the CHANGELOG.md for breaking changes.
# sudo nano CHANGELOG.md
# Re-run the install and start tags to apply updates.
sudo ansible-playbook -i inventory/hosts setup.yml --tags=install-all,startIf you removed components from vars.yml, run a full setup instead so the playbook can uninstall disabled services:
sudo ansible-playbook -i inventory/hosts setup.yml --tags=setup-all,ensure-matrix-users-created,startMigration: Preserve Homeserver Identity
For a fresh homeserver, the normal installation flow above is enough. For a server migration where existing users should keep the same Matrix ID, clients should remain logged in, and bridges should continue to work, copying only vars.yml is not enough.
Preserve at least:
- The Synapse database.
- The Synapse signing key.
- Appservice registration files and tokens.
- Bridge configuration and bridge databases/state.
- The same
matrix_domainand public.well-knowndelegation. - Media that you still care about, especially local profile and room avatars.
| ⚠️ DO NOT RUN BRIDGES TWICE |
When migrating bridge-heavy setups, stop the old bridge services before starting the new ones. Running the same Signal, Telegram, Discord, Instagram, Messenger, or LinkedIn bridge state on two servers at the same time can cause external-service conflicts and duplicate sessions. |
If historical Matrix media is not important, you can keep the migration lightweight by skipping most remote media. Do not forget that avatars are also media. A useful compromise is:
- Keep local media required for account and room avatars.
- Allow old remote cached media to be refetched when possible.
- Enable remote media retention so cached remote media does not grow forever.
In Synapse’s media retention config, the intended policy looks like:
media_retention:
local_media_lifetime: null
remote_media_lifetime: 14dThis keeps local uploads while allowing Synapse to purge remote cached media that has not been accessed recently.
Upgrade Notes for Existing Installations
Fresh installations using the configuration above can skip this subsection. These notes are for servers that were originally deployed with older versions of the playbook.
Playbook migration validation
Older installations can be blocked by the playbook’s migration validation gate:
Your validated migration version (v2026.03.23.0) is behind the expected version (v2026.05.18.0).This is an intentional safety check. Review the relevant playbook changelog entries first, then raise the validated version in your host-specific vars.yml:
matrix_playbook_migration_validated_version: "v2026.05.18.0"The file usually lives at:
inventory/host_vars/matrix.your-domain.com/vars.ymlKetesa configuration for older Synapse Admin setups
The bundled administration UI is now configured with matrix_ketesa_* variables. Existing configurations that still contain matrix_synapse_admin_* variables should be updated:
matrix_ketesa_enabled: true
matrix_ketesa_container_http_host_bind_port: ""
matrix_ketesa_container_labels_traefik_docker_network: proxy
matrix_ketesa_container_labels_traefik_entrypoints: websecure
matrix_ketesa_container_labels_traefik_tls_certResolver: tls_resolverYou can check your configuration with:
grep -nE 'matrix_synapse_admin_|matrix_ketesa_' \
inventory/host_vars/matrix.your-domain.com/vars.ymlIf any matrix_synapse_admin_* variables still appear, rename them to their matrix_ketesa_* equivalents.
Keep the Docker collection current
The setup section already installs community.docker. On older installations, update it before running the playbook:
sudo ansible-galaxy collection install community.docker --upgradeThis avoids errors like:
ERROR! couldn't resolve module/action 'community.docker.docker_image_pull'.Verify that Ansible can resolve the module:
sudo ansible-doc community.docker.docker_image_pullmake roles installs Ansible roles, but it does not necessarily install or upgrade collections. If you see a warning that community.docker does not support your Ansible version and later tasks fail, update Ansible itself, preferably in the dedicated Python virtual environment described above.
Remove the obsolete Telegram public endpoint variable
The Go/bridgev2-based mautrix-telegram bridge no longer has a public web-based login endpoint. Existing configurations must not keep this variable:
matrix_mautrix_telegram_container_labels_public_endpoint_traefik_entrypoints -> <removed (the bridge no longer has a public web-based login endpoint)>Remove or comment out this line:
# matrix_mautrix_telegram_container_labels_public_endpoint_traefik_entrypoints: websecureAfter applying the relevant upgrade notes, run the update again:
sudo ansible-playbook -i inventory/hosts setup.yml --tags=install-all,startUpgrading PostgreSQL
If the playbook notifies you about a new PostgreSQL version being available, you can perform the upgrade with a specific tag.
| ⚠️ BACKUP RECOMMENDED |
Before upgrading the database, ensure you have a recent backup of your data. |
cd /opt/containers/matrix
sudo ansible-playbook -i inventory/hosts setup.yml --tags=upgrade-postgresStopping and Uninstalling
To stop all Matrix services:
cd /opt/containers/matrix
sudo ansible-playbook -i inventory/hosts setup.yml --tags=stopTo completely uninstall and delete all data:
cd /opt/containers/matrix
sudo ansible-playbook -i inventory/hosts setup.yml --tags=uninstall| ⚠️ DATA LOSS |
The |
15. Troubleshooting
Below are common issues and quick checks to resolve them.
| ⚠️ TIP: VERIFY ONE LAYER AT A TIME |
When in doubt, verify DNS → Traefik → Nginx |
15.1 Federation test fails
- Ensure port 8448 is exposed by Traefik and the
synapseentrypoint exists.
cd /opt/containers/traefik-stack
docker compose ps traefik
docker inspect traefik | jq '.[0].NetworkSettings.Ports["8448/tcp"]'- Test the federation version endpoint directly:
curl -sS https://matrix.your-domain.com:8448/_matrix/federation/v1/version | jq15.2 .well-known is invalid or missing
- Verify the
.well-knowndelegation from your root domain (served by Nginx server):
curl -s https://your-domain.com/.well-known/matrix/server | jq
curl -s https://your-domain.com/.well-known/matrix/client | jq- For this Traefik setup,
/.well-known/matrix/servermust return"m.server": "matrix.your-domain.com:8448". A value ending in:443only works if your federation router is intentionally exposed on port 443. - Re-check that the placeholders in
conf/nginx.confwere replaced viasedand that CORS headers middleware uses camelCase keys in Traefik labels (e.g.,accessControlAllowOriginList).
15.3 Traefik/CrowdSec middleware not applied
- Ensure you reference providers correctly:
- Security headers:
security-headers@file - CrowdSec bouncer:
crowdsec-bouncer@docker
- Security headers:
15.4 ACME/TLS issues
- Inspect Traefik logs for ACME errors and DNS problems:
docker compose logs -n 200 traefik | tail -n 200- Confirm A/AAAA records for
traefik.your-domain.comandmatrix.your-domain.compoint to your server and that port 443 is reachable. - Disable CDN Proxy for Initial Certificate: If you are using a CDN like Cloudflare, ensure the proxy is disabled (set to “DNS Only” or “grey cloud”) for your domains during the first certificate request. The HTTP-01 challenge requires Let’s Encrypt to reach your server directly. You can re-enable the proxy after the certificate is issued.
15.5 Docker network not found
- Make sure the external
proxynetwork exists and is used by all services behind Traefik:
docker network ls | grep proxy || docker network create proxy15.6 Permission problems in inventory/host_vars
- If editing files is cumbersome, you can align ownership (optional):
sudo chown -R "$USER":"$USER" /opt/containers/matrix/inventory15.7 Ansible/venv not active
- If you installed Ansible in a virtualenv, reactivate it before running playbooks:
source ~/.venvs/matrix-ansible/bin/activate15.8 CrowdSec bouncer not registered
- Check the bouncer status inside the CrowdSec container (run in your Traefik stack directory):
docker compose exec crowdsec cscli bouncers list15.9 Element Call: MISSING_MATRIX_RTC_TRANSPORT
If Element Web or another client says the server is not configured for Element Call and reports MISSING_MATRIX_RTC_TRANSPORT, the client did not discover your MatrixRTC/LiveKit transport.
When using Element Call with LiveKit, the root-domain /.well-known/matrix/client response must include the RTC focus information in addition to the homeserver delegation:
{
"m.homeserver": {
"base_url": "https://matrix.your-domain.com"
},
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://matrix.your-domain.com/livekit-jwt-service"
}
]
}If the root domain serves .well-known from an Nginx container with a single-file bind mount, replacing the file on the host may leave the running container attached to the old inode. After editing the well-known response, recreate the Nginx container or edit the mounted file in place and reload Nginx.
Conclusion
You now have a fully functional, federated Matrix Synapse homeserver integrated with your Traefik proxy and Nginx web server. This powerful setup not only gives you control over your own secure communications but is also extensible with numerous bridges and features, allowing you to create a central hub for all your chat services.





