e2f578fde5
Add `adminapi` resource to MAS HTTP listener in deploy.sh and quickstart.sh. Without it, MAS never served /api/admin/v1/... causing Element Admin to always throw TypeError: Failed to fetch. Updated docs and added regression test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
503 lines
16 KiB
Bash
Executable File
503 lines
16 KiB
Bash
Executable File
#!/bin/bash
|
|
# Matrix Stack Quick Start
|
|
# Single-machine production deployment with Let's Encrypt.
|
|
# Asks three questions, generates all configs, starts the stack.
|
|
|
|
set -e
|
|
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'; NC='\033[0m'
|
|
|
|
ok() { echo -e "${GREEN}✓${NC} $1"; }
|
|
info() { echo -e "${BLUE}·${NC} $1"; }
|
|
warn() { echo -e "${YELLOW}!${NC} $1"; }
|
|
fail() { echo -e "${RED}✗${NC} $1"; exit 1; }
|
|
|
|
gen_secret() { openssl rand -base64 32 | tr -d "=+/" | cut -c1-32; }
|
|
gen_hex() { openssl rand -hex 32; }
|
|
|
|
sudo docker ps &>/dev/null || fail "Cannot reach Docker. Is it running?"
|
|
|
|
echo ""
|
|
echo "Matrix Stack — Quick Start"
|
|
echo "=========================="
|
|
echo ""
|
|
|
|
# ── Three prompts ───────────────────────────────────────────────────────────
|
|
|
|
read -p "Domain (e.g. example.com): " DOMAIN
|
|
[[ -z "$DOMAIN" ]] && fail "Domain required."
|
|
|
|
read -p "Email for Let's Encrypt: " LETSENCRYPT_EMAIL
|
|
[[ -z "$LETSENCRYPT_EMAIL" ]] && fail "Email required."
|
|
|
|
read -p "Enable Element Call (video/voice)? [y/N]: " _EC
|
|
[[ "$_EC" =~ ^[Yy]$ ]] && USE_ELEMENT_CALL=true || USE_ELEMENT_CALL=false
|
|
|
|
echo ""
|
|
|
|
# ── Derived domains ─────────────────────────────────────────────────────────
|
|
|
|
MATRIX_DOMAIN="matrix.${DOMAIN}"
|
|
ELEMENT_DOMAIN="element.${DOMAIN}"
|
|
ADMIN_DOMAIN="admin.${DOMAIN}"
|
|
AUTH_DOMAIN="auth.${DOMAIN}"
|
|
CALL_DOMAIN="call.${DOMAIN}"
|
|
RTC_DOMAIN="rtc.${DOMAIN}"
|
|
|
|
# ── Check for existing data ──────────────────────────────────────────────────
|
|
|
|
if [[ -d "postgres/data" && "$(ls -A postgres/data 2>/dev/null)" ]] || \
|
|
[[ -f "synapse/data/homeserver.yaml" ]]; then
|
|
warn "Existing data found. quickstart.sh is for fresh installs."
|
|
warn "Continuing will wipe postgres/data, mas/data, and generated configs."
|
|
warn "Your synapse/data media store and bridge configs are preserved."
|
|
read -p "Wipe and continue? [y/N]: " _CONT
|
|
[[ "$_CONT" =~ ^[Yy]$ ]] || exit 0
|
|
sudo docker compose --profile single-machine --profile element-call down 2>/dev/null || true
|
|
sudo rm -rf postgres/data mas/data mas/certs caddy/data caddy/config
|
|
echo ""
|
|
fi
|
|
|
|
# ── Generate secrets ─────────────────────────────────────────────────────────
|
|
|
|
info "Generating secrets..."
|
|
POSTGRES_PASSWORD=$(gen_secret)
|
|
MAS_SECRET_KEY=$(gen_hex)
|
|
SYNAPSE_SHARED_SECRET=$(gen_secret)
|
|
SYNAPSE_CLIENT_SECRET=$(gen_secret)
|
|
$USE_ELEMENT_CALL && LIVEKIT_SECRET=$(gen_secret) || LIVEKIT_SECRET=""
|
|
ok "Secrets ready"
|
|
|
|
info "Generating MAS signing key..."
|
|
openssl genrsa 4096 2>/dev/null | openssl pkcs8 -topk8 -nocrypt > mas-signing.key 2>/dev/null
|
|
MAS_SIGNING_KEY=$(cat mas-signing.key)
|
|
ok "MAS signing key ready"
|
|
echo ""
|
|
|
|
# ── Directories ──────────────────────────────────────────────────────────────
|
|
|
|
mkdir -p postgres/{data,init,config}
|
|
mkdir -p synapse/data
|
|
mkdir -p mas/{config,data,certs}
|
|
mkdir -p element/config
|
|
mkdir -p caddy/{data,config}
|
|
mkdir -p livekit
|
|
mkdir -p bridges/{telegram,whatsapp,signal}/config
|
|
mkdir -p appservices
|
|
chmod 755 mas/config element/config
|
|
|
|
# ── .env ─────────────────────────────────────────────────────────────────────
|
|
|
|
info "Writing .env..."
|
|
cat > .env << EOF
|
|
# Generated by quickstart.sh — $(date -u +"%Y-%m-%d %H:%M UTC")
|
|
DOMAIN=${DOMAIN}
|
|
MATRIX_DOMAIN=${MATRIX_DOMAIN}
|
|
ELEMENT_DOMAIN=${ELEMENT_DOMAIN}
|
|
ADMIN_DOMAIN=${ADMIN_DOMAIN}
|
|
AUTH_DOMAIN=${AUTH_DOMAIN}
|
|
CALL_DOMAIN=${CALL_DOMAIN}
|
|
RTC_DOMAIN=${RTC_DOMAIN}
|
|
|
|
POSTGRES_USER=synapse
|
|
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
|
POSTGRES_DB=synapse
|
|
|
|
MAS_DATABASE_URL=postgresql://synapse:${POSTGRES_PASSWORD}@postgres/mas
|
|
MAS_SECRET_KEY=${MAS_SECRET_KEY}
|
|
|
|
LIVEKIT_SECRET=${LIVEKIT_SECRET}
|
|
|
|
TZ=UTC
|
|
|
|
# Suppress docker compose warnings for optional Authelia vars (not used in quickstart)
|
|
AUTHELIA_JWT_SECRET=
|
|
AUTHELIA_SESSION_SECRET=
|
|
AUTHELIA_STORAGE_ENCRYPTION_KEY=
|
|
EOF
|
|
ok ".env written"
|
|
|
|
# ── MAS config ───────────────────────────────────────────────────────────────
|
|
|
|
info "Writing MAS config..."
|
|
cat > mas/config/config.yaml << EOF
|
|
---
|
|
http:
|
|
listeners:
|
|
- name: web
|
|
resources:
|
|
- name: discovery
|
|
- name: human
|
|
- name: oauth
|
|
- name: compat
|
|
- name: graphql
|
|
playground: true
|
|
- name: assets
|
|
- name: adminapi
|
|
binds:
|
|
- address: '[::]:8080'
|
|
- name: internal
|
|
resources:
|
|
- name: health
|
|
binds:
|
|
- address: '127.0.0.1:8081'
|
|
|
|
public_base: 'https://${AUTH_DOMAIN}/'
|
|
issuer: 'https://${AUTH_DOMAIN}/'
|
|
|
|
database:
|
|
uri: 'postgresql://synapse:${POSTGRES_PASSWORD}@postgres/mas'
|
|
auto_migrate: true
|
|
|
|
secrets:
|
|
encryption: '${MAS_SECRET_KEY}'
|
|
keys:
|
|
- kid: 'key-1'
|
|
algorithm: rs256
|
|
key: |
|
|
EOF
|
|
|
|
echo "$MAS_SIGNING_KEY" | sed 's/^/ /' >> mas/config/config.yaml
|
|
|
|
cat >> mas/config/config.yaml << EOF
|
|
|
|
matrix:
|
|
homeserver: '${MATRIX_DOMAIN}'
|
|
endpoint: 'http://synapse:8008'
|
|
secret: '${SYNAPSE_SHARED_SECRET}'
|
|
|
|
passwords:
|
|
enabled: true
|
|
|
|
email:
|
|
from: '"Matrix" <noreply@${DOMAIN}>'
|
|
transport: smtp
|
|
hostname: 'localhost'
|
|
port: 25
|
|
mode: plain
|
|
|
|
policy:
|
|
registration:
|
|
enabled: true
|
|
require_email: false
|
|
|
|
clients:
|
|
- client_id: '01HQW90Z35CMXFJWQPHC3BGZGQ'
|
|
client_auth_method: none
|
|
redirect_uris:
|
|
- 'https://${ELEMENT_DOMAIN}'
|
|
- 'https://${ELEMENT_DOMAIN}/mobile_guide/'
|
|
- 'io.element.app:/callback'
|
|
|
|
- client_id: '01ADMN00000000000000000000'
|
|
client_auth_method: none
|
|
redirect_uris:
|
|
- 'https://${ADMIN_DOMAIN}/'
|
|
- 'https://${ADMIN_DOMAIN}'
|
|
|
|
- client_id: '0000000000000000000SYNAPSE'
|
|
client_auth_method: client_secret_basic
|
|
client_secret: '${SYNAPSE_CLIENT_SECRET}'
|
|
EOF
|
|
ok "MAS config written"
|
|
|
|
# ── Element Web config ────────────────────────────────────────────────────────
|
|
|
|
info "Writing Element Web config..."
|
|
|
|
if $USE_ELEMENT_CALL; then
|
|
ELEMENT_CALL_FEATURES=',"feature_element_call_video_rooms": true'
|
|
ELEMENT_CALL_BLOCK=',
|
|
"element_call": {
|
|
"url": "https://'"${CALL_DOMAIN}"'",
|
|
"participant_limit": 8,
|
|
"brand": "Element Call"
|
|
}'
|
|
else
|
|
ELEMENT_CALL_FEATURES=''
|
|
ELEMENT_CALL_BLOCK=''
|
|
fi
|
|
|
|
cat > element/config/config.json << EOF
|
|
{
|
|
"default_server_config": {
|
|
"m.homeserver": {
|
|
"base_url": "https://${MATRIX_DOMAIN}",
|
|
"server_name": "${MATRIX_DOMAIN}"
|
|
}
|
|
},
|
|
"brand": "Element",
|
|
"show_labs_settings": true,
|
|
"room_directory": {
|
|
"servers": ["${MATRIX_DOMAIN}"]
|
|
},
|
|
"features": {
|
|
"feature_oidc_aware_navigation": true${ELEMENT_CALL_FEATURES}
|
|
},
|
|
"default_server_name": "${MATRIX_DOMAIN}",
|
|
"disable_custom_urls": false,
|
|
"disable_guests": true${ELEMENT_CALL_BLOCK}
|
|
}
|
|
EOF
|
|
ok "Element Web config written"
|
|
|
|
# ── LiveKit config ────────────────────────────────────────────────────────────
|
|
|
|
if $USE_ELEMENT_CALL; then
|
|
info "Writing LiveKit config..."
|
|
cat > livekit/livekit.yaml << EOF
|
|
port: 7880
|
|
|
|
rtc:
|
|
tcp_port: 7881
|
|
port_range_start: 50100
|
|
port_range_end: 50200
|
|
use_external_ip: true
|
|
|
|
keys:
|
|
livekit-key: ${LIVEKIT_SECRET}
|
|
EOF
|
|
ok "LiveKit config written"
|
|
fi
|
|
|
|
# ── Synapse homeserver.yaml ───────────────────────────────────────────────────
|
|
|
|
if [[ ! -f "synapse/data/homeserver.yaml" ]]; then
|
|
info "Generating Synapse config..."
|
|
sudo docker run --rm \
|
|
-v "$(pwd)/synapse/data:/data" \
|
|
-e SYNAPSE_SERVER_NAME="${MATRIX_DOMAIN}" \
|
|
-e SYNAPSE_REPORT_STATS=no \
|
|
matrixdotorg/synapse:latest generate 2>/dev/null
|
|
ok "Synapse config generated"
|
|
fi
|
|
|
|
info "Patching Synapse config..."
|
|
|
|
# Remove sections that will be re-added fresh
|
|
sed -i '/^database:/,/^[^ ]/{ /^database:/d; /^[^ ]/!d }' synapse/data/homeserver.yaml
|
|
sed -i '/^matrix_authentication_service:/,/^[^ ]/{ /^matrix_authentication_service:/d; /^[^ ]/!d }' synapse/data/homeserver.yaml
|
|
sed -i '/^experimental_features:/,/^[^ ]/{ /^experimental_features:/d; /^[^ ]/!d }' synapse/data/homeserver.yaml
|
|
sed -i '/^enable_registration:/d' synapse/data/homeserver.yaml
|
|
sed -i '/^max_event_delay_duration:/d' synapse/data/homeserver.yaml
|
|
sed -i '/^rc_delayed_event_mgmt:/,/^[^ ]/{ /^rc_delayed_event_mgmt:/d; /^[^ ]/!d }' synapse/data/homeserver.yaml
|
|
|
|
cat >> synapse/data/homeserver.yaml << EOF
|
|
|
|
database:
|
|
name: psycopg2
|
|
args:
|
|
user: synapse
|
|
password: ${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: '${SYNAPSE_SHARED_SECRET}'
|
|
EOF
|
|
|
|
if $USE_ELEMENT_CALL; then
|
|
cat >> synapse/data/homeserver.yaml << EOF
|
|
|
|
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: 20
|
|
EOF
|
|
fi
|
|
|
|
ok "Synapse config patched"
|
|
|
|
# ── Caddyfile ─────────────────────────────────────────────────────────────────
|
|
|
|
info "Writing Caddyfile..."
|
|
|
|
if $USE_ELEMENT_CALL; then
|
|
WELLKNOWN_JSON="{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\"},\"m.authentication\":{\"issuer\":\"https://${AUTH_DOMAIN}/\"},\"org.matrix.msc4143.rtc_foci\":[{\"type\":\"livekit\",\"livekit_service_url\":\"https://${RTC_DOMAIN}/livekit/jwt\"}]}"
|
|
ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${MATRIX_DOMAIN}\"}},\"default_server_name\":\"${MATRIX_DOMAIN}\",\"disable_custom_urls\":false,\"disable_guests\":true,\"features\":{\"feature_oidc_aware_navigation\":true,\"feature_element_call_video_rooms\":true},\"element_call\":{\"url\":\"https://${CALL_DOMAIN}\",\"participant_limit\":8,\"brand\":\"Element Call\"}}"
|
|
else
|
|
WELLKNOWN_JSON="{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\"},\"m.authentication\":{\"issuer\":\"https://${AUTH_DOMAIN}/\"}}"
|
|
ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${MATRIX_DOMAIN}\"}},\"default_server_name\":\"${MATRIX_DOMAIN}\",\"disable_custom_urls\":false,\"disable_guests\":true,\"features\":{\"feature_oidc_aware_navigation\":true}}"
|
|
fi
|
|
|
|
cat > caddy/Caddyfile << EOF
|
|
{
|
|
email ${LETSENCRYPT_EMAIL}
|
|
admin 0.0.0.0:2019
|
|
}
|
|
|
|
${MATRIX_DOMAIN} {
|
|
@wk path /.well-known/matrix/client
|
|
handle @wk {
|
|
header Content-Type application/json
|
|
header Access-Control-Allow-Origin "*"
|
|
respond \`${WELLKNOWN_JSON}\` 200
|
|
}
|
|
|
|
@wk_server path /.well-known/matrix/server
|
|
handle @wk_server {
|
|
header Content-Type application/json
|
|
respond \`{"m.server":"${MATRIX_DOMAIN}:443"}\` 200
|
|
}
|
|
|
|
@preflight {
|
|
method OPTIONS
|
|
path_regexp ^/_matrix/.*\$
|
|
}
|
|
handle @preflight {
|
|
header Access-Control-Allow-Origin "*"
|
|
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
|
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
|
|
header Access-Control-Max-Age "86400"
|
|
respond 204
|
|
}
|
|
|
|
@compat path /_matrix/client/v3/login* /_matrix/client/v3/logout* /_matrix/client/v3/refresh* /_matrix/client/r0/login* /_matrix/client/r0/logout* /_matrix/client/r0/refresh*
|
|
handle @compat {
|
|
header Access-Control-Allow-Origin "*"
|
|
reverse_proxy mas:8080 {
|
|
header_down -Access-Control-Allow-Origin
|
|
}
|
|
}
|
|
|
|
@matrix path_regexp ^/_matrix/.*\$
|
|
handle @matrix {
|
|
header Access-Control-Allow-Origin "*"
|
|
reverse_proxy synapse:8008 {
|
|
header_down -Access-Control-Allow-Origin
|
|
}
|
|
}
|
|
|
|
handle {
|
|
reverse_proxy synapse:8008
|
|
}
|
|
}
|
|
|
|
${AUTH_DOMAIN} {
|
|
@disco path /.well-known/openid-configuration
|
|
handle @disco {
|
|
header Access-Control-Allow-Origin "*"
|
|
reverse_proxy mas:8080
|
|
}
|
|
|
|
@oauth path /oauth2/*
|
|
route @oauth {
|
|
header Access-Control-Allow-Origin "*"
|
|
reverse_proxy mas:8080
|
|
}
|
|
|
|
handle_path /account/* {
|
|
reverse_proxy mas:8080
|
|
}
|
|
|
|
handle {
|
|
reverse_proxy mas:8080
|
|
}
|
|
}
|
|
|
|
${ELEMENT_DOMAIN} {
|
|
@cfg path /config.json
|
|
handle @cfg {
|
|
header Content-Type application/json
|
|
header Cache-Control no-store
|
|
respond \`${ELEMENT_CFG_JSON}\` 200
|
|
}
|
|
|
|
handle {
|
|
reverse_proxy element:80
|
|
}
|
|
}
|
|
|
|
${ADMIN_DOMAIN} {
|
|
reverse_proxy element-admin:8080
|
|
}
|
|
EOF
|
|
|
|
if $USE_ELEMENT_CALL; then
|
|
cat >> caddy/Caddyfile << EOF
|
|
|
|
${RTC_DOMAIN} {
|
|
handle_path /livekit/jwt* {
|
|
reverse_proxy lk-jwt-service:8080
|
|
}
|
|
|
|
handle_path /livekit/sfu* {
|
|
reverse_proxy livekit:7880
|
|
}
|
|
}
|
|
|
|
${CALL_DOMAIN} {
|
|
reverse_proxy element-call:8080
|
|
}
|
|
EOF
|
|
fi
|
|
|
|
ok "Caddyfile written"
|
|
echo ""
|
|
|
|
# ── Start the stack ───────────────────────────────────────────────────────────
|
|
|
|
info "Starting PostgreSQL..."
|
|
sudo docker compose up -d postgres
|
|
|
|
info "Waiting for PostgreSQL..."
|
|
for i in {1..30}; do
|
|
sudo docker compose exec -T postgres pg_isready -U synapse &>/dev/null && break
|
|
[[ $i -eq 30 ]] && fail "PostgreSQL did not become ready in time"
|
|
sleep 2
|
|
done
|
|
ok "PostgreSQL ready"
|
|
|
|
CORE_SERVICES="postgres synapse mas element element-admin caddy"
|
|
if $USE_ELEMENT_CALL; then CORE_SERVICES="${CORE_SERVICES} livekit lk-jwt-service element-call"; fi
|
|
|
|
info "Starting all services..."
|
|
sudo docker compose --profile single-machine up -d ${CORE_SERVICES}
|
|
echo ""
|
|
|
|
# ── Summary ───────────────────────────────────────────────────────────────────
|
|
|
|
ok "Stack is up."
|
|
echo ""
|
|
echo " Element Web: https://${ELEMENT_DOMAIN}"
|
|
echo " Matrix API: https://${MATRIX_DOMAIN}"
|
|
echo " MAS Auth: https://${AUTH_DOMAIN}"
|
|
echo " Element Admin: https://${ADMIN_DOMAIN}"
|
|
if $USE_ELEMENT_CALL; then echo " Element Call: https://${CALL_DOMAIN}"; fi
|
|
echo ""
|
|
echo "DNS — point all subdomains to this server:"
|
|
echo " ${MATRIX_DOMAIN}"
|
|
echo " ${ELEMENT_DOMAIN}"
|
|
echo " ${ADMIN_DOMAIN}"
|
|
echo " ${AUTH_DOMAIN}"
|
|
if $USE_ELEMENT_CALL; then
|
|
echo " ${CALL_DOMAIN}"
|
|
echo " ${RTC_DOMAIN}"
|
|
echo ""
|
|
echo "Firewall — open in addition to 80/443:"
|
|
echo " TCP 7881 (LiveKit WebRTC signaling)"
|
|
echo " UDP 50100-50200 (LiveKit media)"
|
|
fi
|
|
echo "Register your first account at https://${ELEMENT_DOMAIN}"
|
|
echo ""
|
|
echo "To set up messaging bridges (WhatsApp, Signal, Telegram):"
|
|
echo " ./setup-bridges.sh"
|
|
echo ""
|
|
echo "Logs: docker compose logs -f"
|
|
echo "Stop: docker compose --profile single-machine down"
|
|
echo ""
|