#!/bin/sh set -eu BASE_DIR="/opt/matrix" DOCKER_COMPOSE=$(command -v docker-compose || command -v docker compose) || { echo "[ERROR] docker-compose or docker compose not found" >&2 exit 1 } usage() { cat <<'EOF' whiterabbit: auto-configure Matrix homeservers (Dendrite or Synapse) with a global Coturn instance and Nginx + Let's Encrypt proxy. Main actions: turn Install or configure the global Coturn server dendrite Add a new Matrix server (Dendrite) synapse Add a new Matrix server (Synapse) list Show all configured Matrix servers remove Remove a Matrix server by domain EOF exit 1 } require_nonempty() { VAR_NAME="$1" VAR_VALUE="$2" [ -n "$VAR_VALUE" ] || { echo "[ERROR] $VAR_NAME cannot be empty" >&2 exit 1 } } list_servers() { echo "=== Installed servers ===" for dir in "$BASE_DIR"/*; do [ -d "$dir" ] || continue name=$(basename "$dir") status=$(docker ps --filter "name=^${name}$" --format "{{.Status}}") ports=$(docker ps --filter "name=^${name}$" --format "{{.Ports}}" | sed 's/, /\n /g') [ -n "$status" ] && { echo "- $name [RUNNING] on:" echo " $ports" } || echo "- $name [STOPPED]" done } install_turn() { echo "=== Installing global Coturn ===" printf "TURN domain (eg. turn.example.com): " read -r TURN_DOMAIN require_nonempty TURN_DOMAIN "$TURN_DOMAIN" printf "TURN IP (eg. 127.0.0.1): " read -r TURN_SERVER_IP require_nonempty TURN_SERVER_IP "$TURN_SERVER_IP" printf "Listening device (eg. eth0): " read -r TURN_LISTENING_DEVICE require_nonempty TURN_LISTENING_DEVICE "$TURN_LISTENING_DEVICE" printf "TURN shared secret: " read -r TURN_SECRET require_nonempty TURN_SECRET "$TURN_SECRET" mkdir -p /etc/turn cat < /etc/turn/turnserver.conf listening-device=$TURN_LISTENING_DEVICE listening-port=3478 tls-listening-port=5349 listening-ip=$TURN_SERVER_IP min-port=49152 max-port=65535 use-auth-secret static-auth-secret=$TURN_SECRET realm=$TURN_DOMAIN syslog no-rfc5780 no-stun-backward-compatibility response-origin-only-with-rfc5780 EOF echo "$TURN_DOMAIN" > /etc/turn/domain echo "$TURN_SECRET" > /etc/turn/secret case $(command -v systemctl >/dev/null && echo y || echo n) in y) systemctl enable --now coturn || true ;; n) service coturn start || true ;; esac echo "[OK] Global Coturn configured ($TURN_DOMAIN)" } common_prompts() { printf "Matrix domain (eg. matrix.example.com): " read -r MATRIX_DOMAIN require_nonempty MATRIX_DOMAIN "$MATRIX_DOMAIN" printf "Let's Encrypt certificate email: " read -r EMAIL require_nonempty EMAIL "$EMAIL" DB_PASS=$(tr -dc 'A-Za-z0-9' "/etc/nginx/sites-available/$DOMAIN" server { listen 80; server_name $DOMAIN; location /.well-known/matrix/server { default_type application/json; return 200 '{ "m.server": "$DOMAIN:443" }'; } location /.well-known/matrix/client { default_type application/json; return 200 '{ "m.homeserver": { "base_url": "https://$DOMAIN" }, "m.identity_server": { "base_url": "https://vector.im" } }'; } location / { proxy_pass http://127.0.0.1:$SERVICE_PORT; proxy_set_header Host \$host; proxy_set_header X-Forwarded-For \$remote_addr; } } EOF ln -sf "/etc/nginx/sites-available/$DOMAIN" "/etc/nginx/sites-enabled/$DOMAIN" nginx -t || { echo "[ERROR] Invalid nginx config"; exit 1; } case $(command -v systemctl >/dev/null && echo y || echo n) in y) systemctl reload nginx ;; n) nginx -s reload ;; esac case $(command -v certbot >/dev/null && echo y || echo n) in y) certbot --nginx --non-interactive --agree-tos -m "$EMAIL" -d "$DOMAIN" || { echo "[WARN] Certbot failed for $DOMAIN" } ;; n) echo "[WARN] Certbot not installed, skipping TLS setup" ;; esac } install_dendrite() { echo "=== Installing Matrix Dendrite ===" common_prompts INSTALL_DIR="$BASE_DIR/$MATRIX_DOMAIN" mkdir -p "$INSTALL_DIR/config" cd "$INSTALL_DIR" || exit 1 cat < .env POSTGRES_HOSTNAME=postgres POSTGRES_VERSION=15-alpine POSTGRES_USER=dendrite POSTGRES_PASSWORD=$DB_PASS POSTGRES_DB=dendrite MONOLITH_HOSTNAME=monolith MONOLITH_IMAGE=matrixdotorg/dendrite-monolith:latest MONOLITH_PORT_HTTP=$PORT_HTTP MONOLITH_PORT_HTTPS=$PORT_HTTPS EOF cat < compose.yml services: postgres: hostname: \${POSTGRES_HOSTNAME:-postgres} container_name: ${MATRIX_DOMAIN}-db image: postgres:\${POSTGRES_VERSION:-15-alpine} restart: always volumes: - ./dendrite_postgres_data:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-$DB_PASS} POSTGRES_USER: \${POSTGRES_USER:-dendrite} POSTGRES_DB: \${POSTGRES_DB:-dendrite} healthcheck: test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-dendrite}"] interval: 5s timeout: 5s retries: 5 networks: - internal monolith: hostname: \${MONOLITH_HOSTNAME:-monolith} container_name: ${MATRIX_DOMAIN} image: \${MONOLITH_IMAGE:-matrixdotorg/dendrite-monolith:latest} ports: - \${MONOLITH_PORT_HTTP:-8008}:8008 - \${MONOLITH_PORT_HTTPS:-8448}:8448 volumes: - ./config:/etc/dendrite - ./dendrite_media:/var/dendrite/media - ./dendrite_jetstream:/var/dendrite/jetstream - ./dendrite_search_index:/var/dendrite/searchindex - ./:/mnt depends_on: postgres: condition: service_healthy networks: - internal restart: unless-stopped networks: internal: attachable: true volumes: dendrite_postgres_data: dendrite_media: dendrite_jetstream: dendrite_search_index: EOF cat < config/dendrite.yaml version: 2 global: server_name: $MATRIX_DOMAIN private_key: /mnt/matrix_key.pem tls_cert: /mnt/server.crt tls_key: /mnt/server.key old_private_keys: [] key_validity_period: 168h0m0s database: connection_string: postgresql://dendrite:$DB_PASS@postgres/dendrite?sslmode=disable max_open_conns: 90 max_idle_conns: 5 conn_max_lifetime: -1 well_known_server_name: "$MATRIX_DOMAIN:443" well_known_client_name: "https://$MATRIX_DOMAIN" cache: max_size_estimated: 1gb max_age: 1h trusted_third_party_id_servers: - matrix.org - vector.im disable_federation: false report_stats: enabled: false client_api: registration_disabled: true registration_shared_secret: "$REG_SECRET" turn: turn_user_lifetime: "5m" turn_uris: - turn:$TURN_DOMAIN?transport=udp - turn:$TURN_DOMAIN?transport=tcp turn_shared_secret: "$TURN_SECRET" federation_api: send_max_retries: 16 disable_tls_validation: false media_api: base_path: ./media_store max_file_size_bytes: 10485760 sync_api: search: enabled: false index_path: "./searchindex" user_api: auto_join_rooms: - "#main:$MATRIX_DOMAIN" logging: - type: std level: info - type: file level: info params: path: ./logs jetstream: addresses: [] disable_tls_validation: false storage_path: ./ topic_prefix: Dendrite metrics: enabled: false basic_auth: username: metrics password: metrics dns_cache: enabled: false cache_size: 256 cache_lifetime: "5m" EOF echo "[INFO] Generating Dendrite keys..." docker run --rm --entrypoint="" -v $(pwd):/mnt matrixdotorg/dendrite-monolith:latest /usr/bin/generate-keys -private-key /mnt/matrix_key.pem -tls-cert /mnt/server.crt -tls-key /mnt/server.key echo "[OK] Keys generated in $PWD" $DOCKER_COMPOSE up -d configure_nginx "$MATRIX_DOMAIN" "$PORT_HTTP" echo "[OK] Dendrite server ($MATRIX_DOMAIN) is running." } install_synapse() { echo "=== Installing Matrix Synapse ===" common_prompts INSTALL_DIR="$BASE_DIR/$MATRIX_DOMAIN" mkdir -p "$INSTALL_DIR" cd "$INSTALL_DIR" || exit 1 mkdir -p data db chown -R 991:991 data/ chown -R 999:999 db/ DB_USER="synapse" DB_NAME="synapse" DB_PASS=$(openssl rand -hex 16) REG_SECRET=$(openssl rand -hex 32) : "${PORT_HTTP:=8008}" : "${PORT_HTTPS:=8448}" cat < .env # Matrix Synapse environment MATRIX_DOMAIN=$MATRIX_DOMAIN PORT_HTTP=$PORT_HTTP PORT_HTTPS=$PORT_HTTPS DB_USER=$DB_USER DB_NAME=$DB_NAME DB_PASS=$DB_PASS REG_SECRET=$REG_SECRET TURN_DOMAIN=$TURN_DOMAIN TURN_SECRET=$TURN_SECRET EOF cat < compose.yml services: db: image: postgres:15-alpine restart: always environment: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASS} POSTGRES_DB: ${DB_NAME} LANG: C LC_ALL: C command: postgres -c lc_collate=C -c lc_ctype=C volumes: - ./db:/var/lib/postgresql/data networks: - synapse-net synapse: image: matrixdotorg/synapse:latest restart: always depends_on: - db environment: SYNAPSE_SERVER_NAME: ${MATRIX_DOMAIN} SYNAPSE_REPORT_STATS: "yes" ports: - "${PORT_HTTP}:8008" - "${PORT_HTTPS}:8448" volumes: - ./data:/data networks: - synapse-net networks: synapse-net: driver: bridge EOF $DOCKER_COMPOSE --env-file .env up -d docker run -it --rm -v ./data:/data -e SYNAPSE_SERVER_NAME=$MATRIX_DOMAIN -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:latest generate echo "[INFO] Generating homeserver.yaml for Postgres + TURN..." cat < data/homeserver.yaml server_name: $MATRIX_DOMAIN pid_file: /data/homeserver.pid listeners: - port: $PORT_HTTP tls: false type: http x_forwarded: true resources: - names: [client, federation] compress: false database: name: psycopg2 args: user: $DB_USER password: $DB_PASS host: db database: $DB_NAME cp_min: 5 cp_max: 10 enable_registration: false registration_shared_secret: "$REG_SECRET" turn_uris: - turn:$TURN_DOMAIN?transport=udp - turn:$TURN_DOMAIN?transport=tcp turn_shared_secret: "$TURN_SECRET" turn_user_lifetime: "5m" log_config: "/data/$MATRIX_DOMAIN.log.config" media_store_path: /data/media_store report_stats: false macaroon_secret_key: $REG_SECRET form_secret: $REG_SECRET signing_key_path: "/data/$MATRIX_DOMAIN.signing.key" trusted_key_servers: - server_name: "matrix.org" suppress_key_server_warning: true EOF echo "[INFO] Restarting Synapse with full config..." $DOCKER_COMPOSE --env-file .env restart synapse $DOCKER_COMPOSE --env-file .env restart synapse configure_nginx "$MATRIX_DOMAIN" "$PORT_HTTP" echo "[OK] Synapse server ($MATRIX_DOMAIN) is running with Postgres." } remove_server() { [ "$#" -eq 0 ] && echo "Usage: remove_server [domain2 ...]" && return 1 for DOMAIN in "$@"; do SERVER_DIR="$BASE_DIR/$DOMAIN" case "$DOMAIN" in ""|"/"|".") echo "[ERROR] Refusing to delete dangerous path ($DOMAIN)" continue ;; esac [ -d "$SERVER_DIR" ] && \ echo "[INFO] Stopping and removing server: $DOMAIN" && \ (cd "$SERVER_DIR" && $DOCKER_COMPOSE down -v) || \ echo "[WARN] Failed to stop container for $DOMAIN" rm -rf "$SERVER_DIR" rm -f "/etc/nginx/sites-available/$DOMAIN" "/etc/nginx/sites-enabled/$DOMAIN" done nginx -t && (systemctl reload nginx 2>/dev/null || nginx -s reload) echo "[OK] Done removing servers: $*" } CMD="${1:-}" case "$CMD" in -t) install_turn ;; -d) install_dendrite ;; -s) install_synapse ;; -l) list_servers ;; -r) [ -z "$2" ] && { echo "Usage: $0 -r [domain2 ...]"; exit 1; } shift remove_server "$@" ;; turn) install_turn ;; dendrite) install_dendrite ;; synapse) install_synapse ;; list) list_servers ;; remove) [ -z "$2" ] && { echo "Usage: $0 remove [domain2 ...]"; exit 1; } shift remove_server "$@" ;; *) usage ;; esac