This document describes what each component needs and how the automated scripts configure it. Use this when you need to understand, modify, or troubleshoot the generated configuration.
For most users: run ./quickstart.sh (production) or ./deploy.sh (advanced). The scripts handle everything below automatically.
All secrets and domain names. Generated by quickstart.sh or deploy.sh. Do not commit to version control.
Key variables:
| Variable | Purpose |
|---|---|
MATRIX_DOMAIN |
Synapse homeserver domain (e.g. matrix.example.com) |
AUTH_DOMAIN |
MAS domain (e.g. auth.example.com) |
ELEMENT_DOMAIN |
Element Web domain |
ADMIN_DOMAIN |
Element Admin domain |
POSTGRES_PASSWORD |
Shared database password (same across all services) |
MAS_SECRET_KEY |
64-char hex encryption key for MAS |
LIVEKIT_SECRET |
LiveKit API secret (only if Element Call enabled) |
POSTGRES_IMAGE |
PostgreSQL image (default: postgres:16-alpine) |
SYNAPSE_IMAGE |
Synapse image (default: matrixdotorg/synapse:latest) |
ELEMENT_IMAGE |
Element Web image (default: vectorim/element-web:latest) |
ELEMENT_ADMIN_IMAGE |
Element Admin image (default: oci.element.io/element-admin:latest) |
REDIS_IMAGE |
Redis image (default: redis:7-alpine) |
MAS_IMAGE |
MAS image (default: ghcr.io/element-hq/matrix-authentication-service:latest) |
TELEGRAM_IMAGE |
mautrix-telegram image (default: dock.mau.dev/mautrix/telegram:latest) |
WHATSAPP_IMAGE |
mautrix-whatsapp image (default: dock.mau.dev/mautrix/whatsapp:latest) |
SIGNAL_IMAGE |
mautrix-signal image (default: dock.mau.dev/mautrix/signal:latest) |
LIVEKIT_IMAGE |
LiveKit image (default: livekit/livekit-server:latest) |
LK_JWT_IMAGE |
lk-jwt-service image (default: ghcr.io/element-hq/lk-jwt-service:latest) |
ELEMENT_CALL_IMAGE |
Element Call image (default: ghcr.io/element-hq/element-call:latest) |
AUTHELIA_IMAGE |
Authelia image (default: authelia/authelia:latest) |
CADDY_IMAGE |
Caddy image (default: caddy:2-alpine) |
All image variables are optional. If absent, each compose service falls back to the default tag via ${VAR:-default} syntax, so docker compose works without a .env file.
Generated by the official Synapse tool:
docker run --rm \
-v "$(pwd)/synapse/data:/data" \
-e SYNAPSE_SERVER_NAME=matrix.example.com \
-e SYNAPSE_REPORT_STATS=no \
matrixdotorg/synapse:latest generateThe scripts then patch in these sections:
database:
name: psycopg2
args:
user: synapse
password: YOUR_POSTGRES_PASSWORD
database: synapse
host: postgres
port: 5432
cp_min: 5
cp_max: 10
enable_registration: false
matrix_authentication_service:
enabled: true
endpoint: 'http://mas:8080'
secret: 'YOUR_SYNAPSE_SHARED_SECRET'If Element Call is enabled, also:
experimental_features:
msc3266_enabled: true
msc4222_enabled: true
msc4140_enabled: true
max_event_delay_duration: 24h
rc_delayed_event_mgmt:
per_second: 1
burst_count: 20If bridges or double puppet are configured, also:
app_service_config_files:
- /appservices/doublepuppet.yaml
- /bridges/whatsapp/config/registration.yaml
- /bridges/signal/config/registration.yaml
- /bridges/telegram/config/registration.yamlMAS is configured with:
- HTTP listener on
[::]:8080with resources:discovery,human,oauth,compat,graphql,assets - Internal health listener on
127.0.0.1:8081 - PostgreSQL connection
- RSA signing key (generated by
openssl genrsa 4096 | openssl pkcs8 -topk8 -nocrypt) passwords.enabled: true(MAS handles auth directly; set tofalseif using Authelia)
MAS client registrations required:
| Client ID | Purpose |
|---|---|
01HQW90Z35CMXFJWQPHC3BGZGQ |
Element Web (public client) |
01ADMN00000000000000000000 |
Element Admin (public client) |
0000000000000000000SYNAPSE |
Synapse backend (confidential, client_secret_basic) |
The client ID 01ADMN00000000000000000000 is a valid 26-character Crockford base32 ULID. The previously used 01ADMIN000000000000000000 (25 chars, contains I) was invalid and caused YAML parse errors on startup.
Redirect URIs for Element Web:
https://element.yourdomain.com
https://element.yourdomain.com/mobile_guide/
io.element.app:/callback
Redirect URIs for Element Admin:
https://admin.yourdomain.com/
https://admin.yourdomain.com
Served inline by Caddy (not read from the container's filesystem). The Caddyfile intercepts /config.json requests and returns the JSON directly as a respond directive.
Minimum required structure:
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.example.com",
"server_name": "matrix.example.com"
}
},
"default_server_name": "matrix.example.com",
"features": {
"feature_oidc_aware_navigation": true
}
}With Element Call:
{
"features": {
"feature_oidc_aware_navigation": true,
"feature_element_call_video_rooms": true
},
"element_call": {
"url": "https://call.example.com",
"participant_limit": 8,
"brand": "Element Call"
}
}For single-machine production deployments (used by quickstart.sh), Caddy runs in the same Docker network as all other services and reverse-proxies to container names.
Key routing rules on the Matrix domain:
/.well-known/matrix/client→ inline JSON response (includes MAS issuer URL)/.well-known/matrix/server→ inline JSON response/_matrix/client/v3/login*etc. → MAS (compat endpoints)/_matrix/*→ Synapse/config.json(on Element domain) → inline JSON response
The /.well-known/matrix/client response must include:
{
"m.homeserver": {"base_url": "https://matrix.example.com"},
"m.authentication": {"issuer": "https://auth.example.com/"}
}With Element Call, also add:
{
"org.matrix.msc4143.rtc_foci": [
{"type": "livekit", "livekit_service_url": "https://rtc.example.com/livekit/jwt"}
]
}Important: respond body must be on a single line. Caddy does not support multi-line inline response bodies.
For local testing (used by deploy.sh local mode), the Caddyfile uses local_certs globally and tls internal per block instead of Let's Encrypt.
Only needed when Element Call is enabled:
port: 7880
rtc:
tcp_port: 7881
port_range_start: 50100
port_range_end: 50200
use_external_ip: true
keys:
livekit-key: YOUR_LIVEKIT_SECRETThe key name livekit-key must match the LIVEKIT_KEY value in docker-compose.yml (hardcoded as livekit-key).
deploy.sh prompts for two optional image settings:
Custom registry prefix — prepends a registry URL to every image. Useful for air-gapped environments or internal mirrors:
Custom Docker registry prefix: myregistry.example.com
Results in .env entries like:
SYNAPSE_IMAGE=myregistry.example.com/matrixdotorg/synapse:latest
REDIS_IMAGE=myregistry.example.com/redis:7-alpine
Hardened images — uses dhi.io hardened variants for Redis, PostgreSQL, and Caddy. Takes priority over the custom registry prefix for those three images:
REDIS_IMAGE=dhi.io/redis:7
POSTGRES_IMAGE=dhi.io/postgres:16
CADDY_IMAGE=dhi.io/caddy:2
Many enterprise registries act as pull-through caches that mirror images under the full original registry path. For example, Harbor's proxy cache serves Docker Hub images as:
myregistry.example.com/docker.io/library/redis:7-alpine
myregistry.example.com/docker.io/matrixdotorg/synapse:latest
myregistry.example.com/ghcr.io/element-hq/matrix-authentication-service:latest
The custom registry prefix in deploy.sh does not add the docker.io/ or other intermediate path components — it produces myregistry.example.com/redis:7-alpine. If your mirror requires the full path structure, set the image variables manually in .env after running deploy.sh, or configure your registry to serve images without the intermediate path (most registries support this as an alias/rewrite).
The docker-compose.yml uses Docker Compose profiles to make services optional:
| Profile | Services added |
|---|---|
single-machine |
caddy |
element-call |
livekit, lk-jwt-service, element-call |
authelia |
authelia, redis |
quickstart.sh always uses --profile single-machine. deploy.sh selects profiles based on user choices.
| Container port | Host port | Service |
|---|---|---|
| 8008 | 8008 | Synapse (Matrix API + federation) |
| 8448 | 8448 | Synapse (federation, alternate) |
| 8080 | 8080 | MAS |
| 8081 | 8081 | MAS (internal health) |
| 80 | 8090 | Element Web |
| 8080 | 8091 | Element Admin |
| 8080 | 8083 | Element Call |
| 8080 | 8082 | lk-jwt-service |
| 7880 | 7880 | LiveKit |
| 7881/tcp | 7881 | LiveKit WebRTC signaling |
| 50100-50200/udp | 50100-50200 | LiveKit media |
| 80, 443 | 80, 443 | Caddy |
| 2019 | 2019 | Caddy admin API |
| 5432 | — | PostgreSQL (internal only) |
In single-machine production, Caddy is the only externally visible entry point. The other ports are published for multi-machine setups where Caddy runs on a separate server.
A synthetic appservice that allows bridges to act as your Matrix user rather than a bot. Lives at appservices/doublepuppet.yaml:
id: doublepuppet
url: null # null (not empty string) prevents Synapse retry loops
as_token: "YOUR_AS_TOKEN"
hs_token: "YOUR_HS_TOKEN"
sender_localpart: doublepuppet
rate_limited: false
namespaces:
users:
- regex: "@.*:yourdomain.com"
exclusive: falseurl: null is critical. An empty string causes Synapse to repeatedly attempt HTTP transactions to the appservice.
Each bridge generates its own registration.yaml on first successful startup. The file must be:
- Readable by Synapse (
chmod 644) - Mounted into Synapse via
./bridges:/bridges:ro - Listed in
homeserver.yamlunderapp_service_config_files
Bridges must be configured with hostname: 0.0.0.0 (not 127.0.0.1) so Synapse can reach them across Docker containers.
Element Admin requires three environment variables set on the container:
environment:
SERVER_NAME: "matrix.example.com"
OIDC_CLIENT_ID: "01ADMN00000000000000000000"
OIDC_ISSUER: "https://auth.example.com/"It listens on port 8080 internally (recent versions changed from port 80). The corresponding host port is 8091.
If you need to regenerate secrets manually:
# Generic secret (base64, 32 chars)
openssl rand -base64 32 | tr -d "=+/" | cut -c1-32
# Hex secret (for MAS encryption key — must be hex)
openssl rand -hex 32
# MAS signing key (RSA, PKCS8)
openssl genrsa 4096 | openssl pkcs8 -topk8 -nocrypt > mas-signing.keyWhen Authelia is used as an upstream OIDC provider, MAS is configured with:
upstream_oauth2:
providers:
- id: '01HQW90Z35CMXFJWQPHC3BGZGQ'
issuer: 'https://authelia.example.com'
discovery_url: 'http://authelia:9091/.well-known/openid-configuration'
client_id: 'mas-client'
client_secret: 'YOUR_CLIENT_SECRET'
scope: 'openid profile email offline_access'
token_endpoint_auth_method: 'client_secret_basic'
fetch_userinfo: true
claims_imports:
localpart:
action: force
template: '{{ user.preferred_username }}'
displayname:
action: suggest
template: '{{ user.preferred_username }}'
email:
action: force
template: '{{ user.email }}'
set_email_verification: always
passwords:
enabled: falseKey requirements:
fetch_userinfo: trueis mandatory — Authelia serves claims via userinfo, not the ID token- Use
preferred_usernamenotname— Authelia does not provide anameclaim discovery_urluses the internal container name to avoid SSL certificate trust issues
The Authelia client configuration requires all MAS redirect URI patterns to be registered, including the upstream callback URL with the provider ID.