Reviewed-on: https://git.coopcloud.tech/coop-cloud/matrix-synapse/pulls/62 Reviewed-by: 3wordchant <3wordchant@noreply.git.coopcloud.tech>
Matrix (Synapse)
- Category: Apps
- Status: 0, work-in-progress
- Image:
matrixdotorg/synapse, 4, upstream - Healthcheck: Yes
- Backups: No
- Email: Yes
- Tests: No
- SSO: Yes
Basic usage
- Set up Docker Swarm and
abra - Deploy
coop-cloud/traefik abra app new matrix-synapse --secrets(optionally with--passif you'd like to save secrets inpass)abra app config YOURAPPDOMAIN- be sure to change$DOMAINto something that resolves to your Docker swarm boxabra app deploy YOURAPPDOMAIN- Create an initial user:
abra app run YOURAPPDOMAIN app register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008
Tips & Tricks
Create User
register_new_matrix_user -u <username> -k $(cat /var/run/secrets/registration) -p <password>
Set Admin User
abra app cmd YOURAPPDOMAIN db set_admin <adminuser>
Disabling federation
- Use
DISABLE_FEDERATION=1to turn off federation listeners - Don't use
compose.matrix.ymlin your traefik config to keep the federation ports closed
Enabling federation
Federation is on by default (DISABLE_FEDERATION=0). Remote homeservers need a way to discover the host:port that serves your SERVER_NAME.
There are three supported approaches. At least one needs to be working for federation to work (and matrix will fallback between them).
Option 1: built-in well-known (SERVER_NAME = DOMAIN)
Set SERVE_SERVER_WELLKNOWN=true and leave SERVER_NAME unset (defaults to DOMAIN). The recipe's nginx serves /.well-known/matrix/server and /.well-known/matrix/client on DOMAIN.
Suitable when users are e.g. @alice:matrix.example.com.
Option 2: external well-known on SERVER_NAME
Use when you want users to be e.g. @alice:example.com while Synapse runs at matrix.example.com (and SERVER_NAME is served by the same machine that Synapse is running on). Set:
SERVER_NAME=example.com
DOMAIN=matrix.example.com
SERVE_SERVER_WELLKNOWN=false
The two paths that must be served on SERVER_NAME are:
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"}}
Recommended — let this recipe serve them via Traefik by enabling compose.wellknown.yml:
COMPOSE_FILE="$COMPOSE_FILE:compose.wellknown.yml"
This publishes a Traefik router Host(${SERVER_NAME}) && PathPrefix(/.well-known/matrix)
pointing at the matrix nginx, which already serves both files. The path-scoped, high-priority
rule coexists with any apex website that also serves Host(${SERVER_NAME}) — that site keeps
serving everything except /.well-known/matrix. SERVER_NAME must resolve to this Traefik so
ACME can issue its certificate.
Alternative — serve the two files yourself from whatever already hosts example.com.
Option 3: Traefik matrix-federation entrypoint (port 8448)
Use when SERVER_NAME ≠ DOMAIN but you have no separate web service at SERVER_NAME. Remote homeservers fall back to SERVER_NAME:8448 when there's no delegation (also requires SERVER_NAME pointing to same server that matrix is running on).
Requirements:
- traefik
>= 5.1.2+v3.6.15withMATRIX_FEDERATION_ENABLED=1andcompose.matrix.ymlenabled. SERVER_NAMEset in your matrix-synapse env (used by the federation router's Host rule).
With these in place, the recipe publishes a Traefik router on Host(${SERVER_NAME}) via the matrix-federation entrypoint, reusing the existing matrix nginx → synapse path.
Option 4: DNS SRV records (usually not viable here)
For reasons explained below, I might be confused, but I think SRV records usually don't help with co-op cloud matrix deployments.
You should probably prefer Option 2 (well known), but the possibility of SRV is explained below:
Federation can also be delegated with a DNS SRV record on SERVER_NAME instead of well-known:
_matrix-fed._tcp.example.com. 3600 IN SRV 10 0 8448 matrix.example.com. # modern
_matrix._tcp.example.com. 3600 IN SRV 10 0 8448 matrix.example.com. # deprecated, for older peers
The catch is TLS: on the SRV path a remote validates the certificate against SERVER_NAME, not the SRV target. This recipe's Traefik only issues a cert for DOMAIN, so:
- SRV →
DOMAIN:443 fails — the presented cert is forDOMAIN, but the peer requires one forSERVER_NAME. - SRV →
SERVER_NAME:443 collides — Traefik routes TLS by SNI, andHost(SERVER_NAME)on:443is already owned by whatever apex site servesSERVER_NAME. - SRV →
SERVER_NAME:8448 works — the Option 3matrix-federationrouter holds a cert forSERVER_NAME— but that's just Option 3 made explicit (the:8448fallback already works with no SRV record).
Verifying
The canonical test:
Or check the underlying paths directly. They should all return JSON:
# Options 1 & 2 — delegation
curl https://SERVER_NAME/.well-known/matrix/server
# Option 3 — federation endpoint via 8448
curl https://SERVER_NAME:8448/_matrix/key/v2/server
# Confirms Synapse itself is healthy (independent of the path remote servers use)
curl https://DOMAIN/_matrix/key/v2/server
Getting client discovery on a custom domain
Enable compose.wellknown.yml (see Option 2 above) — it serves /.well-known/matrix/client
on SERVER_NAME too, so clients signing in as @alice:example.com auto-discover the homeserver.
Matrix Authentication Service (MAS)
MAS is Element’s OAuth/OIDC-native auth service for Matrix: it handles login, tokens, and upstream IdPs while Synapse delegates authentication via matrix_authentication_service.
Important
If you plan to migrate an existing homeserver with
syn2mas: deploy and configure MAS as below, but leaveMAS_ENABLED=1commented until migration and cutover are done, so Synapse keeps using your current login path until you intentionally switch. You cannot use Synapse legacy OIDC/Keycloak SSO alongside MAS; plan IdP apps and envs accordingly.
Enable the stack:
- In
.env, uncommentcompose.mas.yml(andcompose.mas-upstream.ymlplus upstream envs if you use an external IdP), and uncomment theSECRET_MAS_*version lines. abra app secret generate YOURAPPDOMAINabra app cmd -l YOURAPPDOMAIN generate_mas_signing_rsa— generates and inserts the PEM RSA key forSECRET_MAS_SIGNING_RSA_VERSION. Requiresopensslon the local machine.abra app cmd YOURAPPDOMAIN db ensure_mas_database(once, creates themasdatabase in Postgres)abra app deploy YOURAPPDOMAIN
Migrating an existing server (syn2mas)
Requires PostgreSQL on Synapse and a dedicated MAS database. Backup Postgres (and configs) before you start. Official background: MAS migration guide.
-
Prepare (Synapse still running): With MAS in
COMPOSE_FILEbutMAS_ENABLEDstill off, deploy, then run checks from your machine:abra app cmd YOURAPPDOMAIN prepare_mas_migrationThis fetches rendered
homeserver.yamlinto the MAS container, runssyn2mas check, thenmigrate --dry-run(the dry run rolls back MAS data at the end). The file stays in the MAS container until next restart, so you can repeat this step to provide the file for the actual migration. -
Optional snapshot: save a copy of the rendered config while
appis up, e.g.abra app run -t YOURAPPDOMAIN app cat /data/homeserver.yaml > homeserver.snapshot.yaml. -
Downtime — stop Synapse: run on the host with Docker/Swarm access (not inside a container), e.g.:
docker service scale <STACK_NAME>_app=0Use the real service name from
docker service ls(suffix_app). -
Migration: with MAS still running and Synapse at zero replicas, run
run_mas_migrationfrom your machine. The homeserver snapshot at/tmp/homeserver.yamlinmasmust still be present from step 1.abra app cmd YOURAPPDOMAIN run_mas_migration -
Cutover: in
.env, setMAS_ENABLED=1,PASSWORD_LOGIN_ENABLED=false, remove legacy Keycloak/SSO envs, thenabra app deploy YOURAPPDOMAIN(Synapse comes back with MAS delegation).syn2masdoes not write to the Synapse database; if you abort before serving traffic through MAS, you can often drop and recreate the MAS DB and revert env.
Bridges
For all Bridges:
- Setting it up is a bit of a chicken/egg & chasing cats moment.
- Make sure to uncomment
APP_SERVICES_ENABLED,HOMESERVER_URL,HOMESERVER_DOMAIN,compose.shared_secret_auth.yml,SHARED_SECRET_AUTH_ENABLEDandSECRET_SHARED_SECRET_AUTH_VERSION - include the registration in synapse, e.g.
APP_SERVICE_CONFIGS="[\"/telegram-data/registration.yaml\"]" - and set yourself as admin, e.g.:
TELEGRAM_BRIDGE_PERMISSIONS="{ \"*\": \"relaybot\", \"@akadmin:example.com\": \"admin\"}"
Important
The shared secret authenticator may break when matrix-synapse uses a newer python version with an error stating something like "module not found". You have to fix the path in the compose.shared_secret_auth.yml like here
Telegram bridging
You need to get your bot setup on the telegram side first by creating a telegram app and a telegram bot and have these values:
api_id: ...
api_hash: ...
telegram_bot_token: ...
Experimental script for a automated token replacement:
DOMAIN=<domain>
abra app secret insert $DOMAIN telegram_api_hash v1 <secret>
abra app secret insert $DOMAIN telegram_bot_token v1 <secret>
abra app secret generate -a $DOMAIN
abra app deploy $DOMAIN
abra app cmd -l $DOMAIN set_bridge_tokens telegram
Alternatively a manual guide for the necessary steps:
DOMAIN=<domain>
abra app secret insert $DOMAIN telegram_api_hash v1 <secret>
abra app secret insert $DOMAIN telegram_bot_token v1 <secret>
abra app secret generate -a $DOMAIN
abra app deploy $DOMAIN
abra app run $DOMAIN telegrambridge cat /data/registration.yaml
abra app undeploy $DOMAIN
abra app secret rm $DOMAIN telegram_as_token
abra app secret insert $DOMAIN telegram_as_token v1 <secret>
abra app secret rm $DOMAIN telegram_hs_token
abra app secret insert $DOMAIN telegram_hs_token v1 <secret>
abra app deploy $DOMAIN
Some helpful documentation:
Discord bridging
WIP docs
Just as messy as the Telegram bridging above! Rough guide:
- get a local copy of
config.yaml - fill it out with the values you need, all the discord token stuff, etc.
- run
mkdir -p data && cp config.yaml data/thendocker run --rm -v data:/data halfshot/matrix-appservice-discord:v1.0.0 sh -c "cd /data && node /build/src/discordas.js -r -u "http://discordbridge:9005" -c config.yaml" - this generates the app service registration configuration you need to feed to the homeserver
- run secret generation for the
discord_db_password, insert yourdiscord_bot_token - run
abra app cp <domain> discord-registration.yaml app:/discord-data(it has to be calleddiscord-registration.yaml) - deploy the bridge & happy hacking
Some helpful documentation:
Signal bridging
Experimental script for a more automated token replacement:
DOMAIN=<domain>
abra app secret generate -a $DOMAIN
abra app deploy $DOMAIN
abra app cmd -l $DOMAIN set_bridge_tokens signal
Alternatively a manual guide for the necessary steps:
DOMAIN=<domain>
abra app secret insert $DOMAIN signal_hs_token v1 foo
abra app secret insert $DOMAIN signal_as_token v1 foo
abra app secret generate $DOMAIN -a
abra app deploy $DOMAIN
abra app run $DOMAIN signalbridge cat /data/registration.yaml
abra app secret rm $DOMAIN signal_as_token
abra app secret insert $DOMAIN signal_as_token v1 <secret>
abra app secret rm $DOMAIN signal_hs_token
abra app secret insert $DOMAIN signal_hs_token v1 <secret>
abra app deploy $DOMAIN
- message
@signalbot:example.comto test - See the docs for authentication