Initial commit: Matrix stack (Synapse + Synapse-Admin + Element Web) behind Traefik
Fully dockerized deployment with automatic Let's Encrypt certificates via Traefik's static file provider (no docker.sock needed), bind-mounted data folders instead of named volumes, and domain/secrets kept out of git via .env + rendered templates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
39
.env.example
Normal file
39
.env.example
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Matrix stack configuration
|
||||||
|
# Copy this file to ".env" and fill in real values before starting the stack.
|
||||||
|
# ".env" is git-ignored on purpose: it is the only place where your real
|
||||||
|
# domain name and secrets live.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Domain ------------------------------------------------------------------
|
||||||
|
# Bare domain used as the Matrix server_name (this is what ends up in every
|
||||||
|
# user id: @user:DOMAIN). A DNS A/AAAA record for DOMAIN and for each
|
||||||
|
# subdomain below must point to this server's IP.
|
||||||
|
DOMAIN=example.com
|
||||||
|
|
||||||
|
# Subdomains (each needs its own DNS record pointing here)
|
||||||
|
MATRIX_SUBDOMAIN=matrix
|
||||||
|
ELEMENT_SUBDOMAIN=element
|
||||||
|
ADMIN_SUBDOMAIN=admin
|
||||||
|
TRAEFIK_SUBDOMAIN=traefik
|
||||||
|
|
||||||
|
# --- Traefik / Let's Encrypt ---------------------------------------------------
|
||||||
|
# Email used for Let's Encrypt account registration and expiry notices.
|
||||||
|
ACME_EMAIL=changeme@example.com
|
||||||
|
|
||||||
|
# htpasswd credentials for the Traefik dashboard (traefik.DOMAIN).
|
||||||
|
# Generate your own with:
|
||||||
|
# openssl passwd -apr1
|
||||||
|
# Format: user:hash (escape any literal "$" as "$$" if you edit this by hand
|
||||||
|
# outside of a real .env file consumed by docker compose)
|
||||||
|
TRAEFIK_DASHBOARD_AUTH='admin:changeme'
|
||||||
|
|
||||||
|
# --- PostgreSQL (Synapse database) -------------------------------------------
|
||||||
|
POSTGRES_DB=synapse
|
||||||
|
POSTGRES_USER=synapse
|
||||||
|
POSTGRES_PASSWORD=changeme
|
||||||
|
|
||||||
|
# --- Synapse -------------------------------------------------------------------
|
||||||
|
# Shared secret used only once, to create the first admin account via
|
||||||
|
# `register_new_matrix_user`. Generate with: openssl rand -hex 32
|
||||||
|
REGISTRATION_SHARED_SECRET=changeme
|
||||||
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Real secrets / domain — never commit
|
||||||
|
.env
|
||||||
|
CREDENTIALS.txt
|
||||||
|
|
||||||
|
# Traefik ACME storage (contains private keys + certificates)
|
||||||
|
traefik/acme/acme.json
|
||||||
|
|
||||||
|
# Runtime data (bind-mounted host folders, not named volumes)
|
||||||
|
postgres/data/*
|
||||||
|
!postgres/data/.gitkeep
|
||||||
|
|
||||||
|
synapse/data/*
|
||||||
|
!synapse/data/.gitkeep
|
||||||
|
|
||||||
|
# Rendered configs (contain the real domain) — templates are tracked instead
|
||||||
|
element/config/config.json
|
||||||
|
synapse-admin/config/config.json
|
||||||
|
wellknown/html/well-known-matrix-client.json
|
||||||
|
traefik/dynamic/routes.yml
|
||||||
118
README.md
Normal file
118
README.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# Matrix stack (Synapse + Synapse-Admin + Element Web) behind Traefik
|
||||||
|
|
||||||
|
A self-hosted Matrix homeserver, its admin panel, and a web client, all
|
||||||
|
running in Docker, fronted by Traefik with automatic Let's Encrypt
|
||||||
|
certificates. No container publishes a port to the host except Traefik
|
||||||
|
(80/443). All persistent data lives in plain host folders under this
|
||||||
|
project (no named Docker volumes), and the real domain name and secrets
|
||||||
|
live only in a local, git-ignored `.env` file.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet ──80/443──> Traefik ──(docker network, no published ports)──┬──> Synapse (matrix.DOMAIN)
|
||||||
|
├──> Element Web (element.DOMAIN)
|
||||||
|
├──> Synapse-Admin (admin.DOMAIN)
|
||||||
|
├──> nginx: /.well-known/matrix/client (DOMAIN)
|
||||||
|
└──> Traefik dashboard (traefik.DOMAIN, basic auth)
|
||||||
|
Synapse ──> PostgreSQL (internal network only)
|
||||||
|
```
|
||||||
|
|
||||||
|
Matrix `server_name` is the bare domain (`DOMAIN`), so user IDs look like
|
||||||
|
`@alice:DOMAIN`. The homeserver's actual HTTP API is served from
|
||||||
|
`matrix.DOMAIN`, discovered by clients via `.well-known/matrix/client`
|
||||||
|
delegation served from the bare domain. **Federation is intentionally
|
||||||
|
disabled** (private/closed server) — see `synapse/data/homeserver.yaml`.
|
||||||
|
|
||||||
|
Traefik uses its static **file provider** for routing (not the Docker
|
||||||
|
provider / `docker.sock`), so Traefik never needs access to the Docker
|
||||||
|
socket — a smaller attack surface, and it sidesteps Docker-API-version
|
||||||
|
compatibility issues some environments have with Traefik's Docker
|
||||||
|
provider.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker Engine + Docker Compose v2
|
||||||
|
- A domain name with DNS **A/AAAA records pointing at this server** for:
|
||||||
|
- `DOMAIN` (e.g. `example.com`)
|
||||||
|
- `matrix.DOMAIN`
|
||||||
|
- `element.DOMAIN`
|
||||||
|
- `admin.DOMAIN`
|
||||||
|
- `traefik.DOMAIN`
|
||||||
|
(subdomain names are configurable in `.env`)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Copy the environment template and fill in your real domain/secrets:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
Edit `.env`:
|
||||||
|
- `DOMAIN` and the four `*_SUBDOMAIN` values
|
||||||
|
- `ACME_EMAIL` — a real address you control (used by Let's Encrypt)
|
||||||
|
- `TRAEFIK_DASHBOARD_AUTH` — generate with `openssl passwd -apr1`
|
||||||
|
- `POSTGRES_PASSWORD` — generate with `openssl rand -hex 24`
|
||||||
|
- `REGISTRATION_SHARED_SECRET` — generate with `openssl rand -hex 32`
|
||||||
|
(this one gets baked into `homeserver.yaml` in step 3 below; keep
|
||||||
|
both in sync if you change it later)
|
||||||
|
|
||||||
|
2. Render the templated config files (Element, Synapse-Admin, the
|
||||||
|
well-known JSON, and Traefik's static routes) with your real domain:
|
||||||
|
```bash
|
||||||
|
./scripts/render-templates.sh
|
||||||
|
```
|
||||||
|
Re-run this any time you change domain-related values in `.env`.
|
||||||
|
|
||||||
|
3. Generate the initial Synapse config (first time only):
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
-e SYNAPSE_SERVER_NAME=<your DOMAIN> \
|
||||||
|
-e SYNAPSE_REPORT_STATS=no \
|
||||||
|
-v "$(pwd)/synapse/data:/data" \
|
||||||
|
matrixdotorg/synapse:latest generate
|
||||||
|
```
|
||||||
|
Then edit `synapse/data/homeserver.yaml`:
|
||||||
|
- Remove `federation` from the listener's `resources` (already disabled
|
||||||
|
by `federation_domain_whitelist: []`, this drops the endpoint too)
|
||||||
|
- Replace the `database:` block with a `psycopg2` block pointing at
|
||||||
|
the `postgres` service, using the credentials from `.env`
|
||||||
|
- Add `public_baseurl: "https://matrix.<DOMAIN>/"`
|
||||||
|
- Add `federation_domain_whitelist: []` and `enable_registration: false`
|
||||||
|
|
||||||
|
(This repo's own deployment already has this file populated under
|
||||||
|
`synapse/data/` locally — it's git-ignored, so a fresh clone always
|
||||||
|
starts from scratch here.)
|
||||||
|
|
||||||
|
4. Fix data directory ownership so the containers can write to their
|
||||||
|
bind-mounted folders:
|
||||||
|
```bash
|
||||||
|
chown -R 70:70 postgres/data # postgres container's uid/gid
|
||||||
|
touch traefik/acme/acme.json && chmod 600 traefik/acme/acme.json
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Start everything:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
Watch `docker compose logs -f traefik` until certificates are issued.
|
||||||
|
|
||||||
|
6. Create your first (admin) user:
|
||||||
|
```bash
|
||||||
|
docker compose exec synapse register_new_matrix_user \
|
||||||
|
-c /data/homeserver.yaml http://localhost:8008 -a
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Federation is off.** This is a private server for your own users only.
|
||||||
|
To enable it, remove `federation_domain_whitelist: []` (or populate it),
|
||||||
|
add `federation` back to the listener's `resources`, and expose the
|
||||||
|
federation port through a Traefik router (typically still 443 via
|
||||||
|
`.well-known/matrix/server` delegation, or a dedicated `8448` entrypoint).
|
||||||
|
- **Registration is closed.** New accounts are created via
|
||||||
|
`register_new_matrix_user` (or through Synapse-Admin once logged in as
|
||||||
|
an admin), not via public sign-up in Element.
|
||||||
|
- **Traefik dashboard** is reachable at `https://traefik.DOMAIN`, behind
|
||||||
|
HTTP basic auth (`TRAEFIK_DASHBOARD_AUTH` in `.env`).
|
||||||
|
- All data directories (`postgres/data`, `synapse/data`) are plain host
|
||||||
|
folders, not Docker named volumes — back them up like any other files.
|
||||||
103
docker-compose.yml
Normal file
103
docker-compose.yml
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
networks:
|
||||||
|
web:
|
||||||
|
name: matrix_web
|
||||||
|
internal:
|
||||||
|
name: matrix_internal
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:v3.5
|
||||||
|
container_name: matrix-traefik
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
environment:
|
||||||
|
- TRAEFIK_DASHBOARD_AUTH=${TRAEFIK_DASHBOARD_AUTH}
|
||||||
|
command:
|
||||||
|
- --api.dashboard=true
|
||||||
|
- --providers.file.directory=/etc/traefik/dynamic
|
||||||
|
- --providers.file.watch=true
|
||||||
|
- --entrypoints.web.address=:80
|
||||||
|
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
||||||
|
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
||||||
|
- --entrypoints.web.http.redirections.entrypoint.permanent=true
|
||||||
|
- --entrypoints.websecure.address=:443
|
||||||
|
- --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}
|
||||||
|
- --certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json
|
||||||
|
- --certificatesresolvers.letsencrypt.acme.httpchallenge=true
|
||||||
|
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
|
||||||
|
- --log.level=INFO
|
||||||
|
- --accesslog=true
|
||||||
|
volumes:
|
||||||
|
- ./traefik/dynamic:/etc/traefik/dynamic:ro
|
||||||
|
- ./traefik/acme:/etc/traefik/acme
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: matrix-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=${POSTGRES_DB}
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||||
|
- POSTGRES_INITDB_ARGS=--encoding=UTF8 --locale=C
|
||||||
|
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||||
|
volumes:
|
||||||
|
- ./postgres/data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
synapse:
|
||||||
|
image: matrixdotorg/synapse:latest
|
||||||
|
container_name: matrix-synapse
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
- internal
|
||||||
|
volumes:
|
||||||
|
- ./synapse/data:/data
|
||||||
|
|
||||||
|
element:
|
||||||
|
image: vectorim/element-web:latest
|
||||||
|
container_name: matrix-element
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
volumes:
|
||||||
|
- ./element/config/config.json:/app/config.json:ro
|
||||||
|
|
||||||
|
synapse-admin:
|
||||||
|
image: awesometechnologies/synapse-admin:latest
|
||||||
|
container_name: matrix-synapse-admin
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
volumes:
|
||||||
|
- ./synapse-admin/config/config.json:/app/config.json:ro
|
||||||
|
|
||||||
|
wellknown:
|
||||||
|
image: nginx:1.27-alpine
|
||||||
|
container_name: matrix-wellknown
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
volumes:
|
||||||
|
- ./wellknown/html:/usr/share/nginx/html:ro
|
||||||
|
- ./wellknown/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
39
element/config/config.json.template
Normal file
39
element/config/config.json.template
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"default_server_config": {
|
||||||
|
"m.homeserver": {
|
||||||
|
"base_url": "https://${MATRIX_SUBDOMAIN}.${DOMAIN}",
|
||||||
|
"server_name": "${DOMAIN}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default_server_name": "${DOMAIN}",
|
||||||
|
"disable_custom_urls": false,
|
||||||
|
"disable_guests": true,
|
||||||
|
"disable_login_language_selector": false,
|
||||||
|
"disable_3pid_login": false,
|
||||||
|
"force_verification": false,
|
||||||
|
"brand": "Element",
|
||||||
|
"integrations_ui_url": "",
|
||||||
|
"integrations_rest_url": "",
|
||||||
|
"integrations_widgets_urls": [],
|
||||||
|
"default_widget_container_height": 280,
|
||||||
|
"default_country_code": "FR",
|
||||||
|
"show_labs_settings": false,
|
||||||
|
"features": {},
|
||||||
|
"default_federate": false,
|
||||||
|
"default_theme": "light",
|
||||||
|
"room_directory": {
|
||||||
|
"servers": []
|
||||||
|
},
|
||||||
|
"enable_presence_by_hs_url": {},
|
||||||
|
"setting_defaults": {
|
||||||
|
"breadcrumbs": true
|
||||||
|
},
|
||||||
|
"jitsi": {
|
||||||
|
"preferred_domain": "meet.element.io"
|
||||||
|
},
|
||||||
|
"element_call": {
|
||||||
|
"brand": "Element Call",
|
||||||
|
"disable": false,
|
||||||
|
"use_exclusively": false
|
||||||
|
}
|
||||||
|
}
|
||||||
0
postgres/data/.gitkeep
Normal file
0
postgres/data/.gitkeep
Normal file
25
scripts/render-templates.sh
Executable file
25
scripts/render-templates.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Renders *.template files into their real config counterparts using the
|
||||||
|
# values from .env. Run this after every edit of .env, before
|
||||||
|
# `docker compose up`.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "Missing .env — copy .env.example to .env and edit it first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source .env
|
||||||
|
set +a
|
||||||
|
|
||||||
|
VARS='${DOMAIN} ${MATRIX_SUBDOMAIN} ${ELEMENT_SUBDOMAIN} ${ADMIN_SUBDOMAIN} ${TRAEFIK_SUBDOMAIN}'
|
||||||
|
|
||||||
|
envsubst "$VARS" < element/config/config.json.template > element/config/config.json
|
||||||
|
envsubst "$VARS" < synapse-admin/config/config.json.template > synapse-admin/config/config.json
|
||||||
|
envsubst "$VARS" < wellknown/well-known-matrix-client.json.template > wellknown/html/well-known-matrix-client.json
|
||||||
|
envsubst "$VARS" < traefik/dynamic/routes.yml.template > traefik/dynamic/routes.yml
|
||||||
|
|
||||||
|
echo "Rendered config for domain: ${DOMAIN}"
|
||||||
3
synapse-admin/config/config.json.template
Normal file
3
synapse-admin/config/config.json.template
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"restrictBaseUrl": ["https://${MATRIX_SUBDOMAIN}.${DOMAIN}"]
|
||||||
|
}
|
||||||
0
synapse/data/.gitkeep
Normal file
0
synapse/data/.gitkeep
Normal file
17
traefik/dynamic/middlewares.yml
Normal file
17
traefik/dynamic/middlewares.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
secure-headers:
|
||||||
|
headers:
|
||||||
|
stsSeconds: 31536000
|
||||||
|
stsIncludeSubdomains: true
|
||||||
|
stsPreload: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
browserXssFilter: true
|
||||||
|
frameDeny: true
|
||||||
|
referrerPolicy: "strict-origin-when-cross-origin"
|
||||||
|
|
||||||
|
dashboard-auth:
|
||||||
|
basicAuth:
|
||||||
|
users:
|
||||||
|
- '{{ env "TRAEFIK_DASHBOARD_AUTH" }}'
|
||||||
73
traefik/dynamic/routes.yml.template
Normal file
73
traefik/dynamic/routes.yml.template
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
http:
|
||||||
|
routers:
|
||||||
|
synapse:
|
||||||
|
rule: "Host(`${MATRIX_SUBDOMAIN}.${DOMAIN}`)"
|
||||||
|
entrypoints:
|
||||||
|
- websecure
|
||||||
|
service: synapse
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
middlewares:
|
||||||
|
- secure-headers
|
||||||
|
|
||||||
|
element:
|
||||||
|
rule: "Host(`${ELEMENT_SUBDOMAIN}.${DOMAIN}`)"
|
||||||
|
entrypoints:
|
||||||
|
- websecure
|
||||||
|
service: element
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
middlewares:
|
||||||
|
- secure-headers
|
||||||
|
|
||||||
|
synapse-admin:
|
||||||
|
rule: "Host(`${ADMIN_SUBDOMAIN}.${DOMAIN}`)"
|
||||||
|
entrypoints:
|
||||||
|
- websecure
|
||||||
|
service: synapse-admin
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
middlewares:
|
||||||
|
- secure-headers
|
||||||
|
|
||||||
|
wellknown:
|
||||||
|
rule: "Host(`${DOMAIN}`)"
|
||||||
|
entrypoints:
|
||||||
|
- websecure
|
||||||
|
service: wellknown
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
middlewares:
|
||||||
|
- secure-headers
|
||||||
|
|
||||||
|
dashboard:
|
||||||
|
rule: "Host(`${TRAEFIK_SUBDOMAIN}.${DOMAIN}`)"
|
||||||
|
entrypoints:
|
||||||
|
- websecure
|
||||||
|
service: api@internal
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
middlewares:
|
||||||
|
- dashboard-auth
|
||||||
|
- secure-headers
|
||||||
|
|
||||||
|
services:
|
||||||
|
synapse:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://matrix-synapse:8008"
|
||||||
|
|
||||||
|
element:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://matrix-element:80"
|
||||||
|
|
||||||
|
synapse-admin:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://matrix-synapse-admin:80"
|
||||||
|
|
||||||
|
wellknown:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://matrix-wellknown:80"
|
||||||
15
wellknown/nginx.conf
Normal file
15
wellknown/nginx.conf
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
location = /.well-known/matrix/client {
|
||||||
|
default_type application/json;
|
||||||
|
add_header Access-Control-Allow-Origin * always;
|
||||||
|
try_files /well-known-matrix-client.json =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
wellknown/well-known-matrix-client.json.template
Normal file
5
wellknown/well-known-matrix-client.json.template
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"m.homeserver": {
|
||||||
|
"base_url": "https://${MATRIX_SUBDOMAIN}.${DOMAIN}"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user