#!/bin/sh set -eu BASE_DIR="/opt/matrix" # Detect docker compose binary DOCKER_COMPOSE=$(command -v docker-compose || command -v "docker" compose || true) if [ -z "$DOCKER_COMPOSE" ]; then echo "[ERROR] docker-compose or docker compose not found" >&2 exit 1 fi 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 Notes: • Coturn should be installed only once using 'turn'. • All Matrix servers share the same Coturn (TURN domain/secret). • TLS certificates are issued automatically using Let's Encrypt. • Requirements: docker, docker-compose, nginx, certbot, coturn. EOF exit 1 } require_nonempty() { VAR_NAME="$1" VAR_VALUE="$2" if [ -z "$VAR_VALUE" ]; then echo "[ERROR] $VAR_NAME cannot be empty" >&2 exit 1 fi } list_servers() { echo "=== Installed servers ===" ls -1 "$BASE_DIR" 2>/dev/null || echo "No servers installed" } 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 if command -v systemctl >/dev/null; then systemctl enable --now coturn || true else service coturn start || true fi 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" printf "Postgres secret: " read -r DB_PASS require_nonempty DB_PASS "$DB_PASS" printf "registration_shared_secret: " read -r REG_SECRET require_nonempty REG_SECRET "$REG_SECRET" printf "MATRIX server IP (eg. 127.0.0.1): " read -r MATRIX_SERVER_IP require_nonempty MATRIX_SERVER_IP "$MATRIX_SERVER_IP" TURN_DOMAIN=$(cat /etc/turn/domain) TURN_SECRET=$(cat /etc/turn/secret) # Generate unique port numbers from hash of domain HASH=$(printf "%s" "$MATRIX_DOMAIN" | cksum | cut -d ' ' -f1) PORT_BASE=$(( (HASH % 1000) + 8000 )) PORT_HTTP=$PORT_BASE PORT_HTTPS=$((PORT_BASE+1)) } configure_nginx() { DOMAIN="$1" SERVICE_PORT="$2" mkdir -p /etc/nginx/sites-available mkdir -p /etc/nginx/sites-enabled cat < "/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; } if command -v systemctl >/dev/null; then systemctl reload nginx else nginx -s reload fi if command -v certbot >/dev/null; then certbot --nginx --non-interactive --agree-tos -m "$EMAIL" -d "$DOMAIN" || { echo "[WARN] Certbot failed for $DOMAIN" } else echo "[WARN] Certbot not installed, skipping TLS setup" fi echo "[OK] Nginx and SSL certificate configured for $DOMAIN" } 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} 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} 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 # --- Plik konfiguracji Dendrite --- 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 # Generate necessary project directory $DOCKER_COMPOSE up -d $DOCKER_COMPOSE down # --- Generowanie kluczy Dendrite + TLS jeśli nie istnieją --- 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" # --- Start kontenerów --- $DOCKER_COMPOSE up -d # --- Konfiguracja Nginx + SSL/TLS --- 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 cat < docker-compose.yml version: '3.4' services: db: image: postgres:15-alpine environment: POSTGRES_USER: synapse POSTGRES_PASSWORD: $DB_PASS POSTGRES_DB: synapse volumes: - ./db:/var/lib/postgresql/data synapse: image: matrixdotorg/synapse:latest depends_on: [ db ] environment: SYNAPSE_SERVER_NAME: $MATRIX_DOMAIN SYNAPSE_REPORT_STATS: "yes" POSTGRES_PASSWORD: $DB_PASS ports: - $PORT_HTTP:8008 - $PORT_HTTPS:8448 volumes: - ./data:/data EOF $DOCKER_COMPOSE up -d cat < data/homeserver-extra.yaml turn_uris: [ "turn:$TURN_DOMAIN?transport=udp", "turn:$TURN_DOMAIN?transport=tcp" ] turn_shared_secret: "$TURN_SECRET" turn_user_lifetime: "5m" EOF configure_nginx "$MATRIX_DOMAIN" "$PORT_HTTP" echo "[OK] Synapse server ($MATRIX_DOMAIN) is running." } remove_server() { DOMAIN="$1" [ -z "$DOMAIN" ] && usage SERVER_DIR="$BASE_DIR/$DOMAIN" case "$DOMAIN" in ""|"/"|".") echo "[ERROR] Refusing to delete dangerous path ($DOMAIN)"; exit 1 ;; esac if [ -d "$SERVER_DIR" ]; then (cd "$SERVER_DIR" && $DOCKER_COMPOSE down -v) || true rm -rf "$SERVER_DIR" fi rm -f "/etc/nginx/sites-available/$DOMAIN" "/etc/nginx/sites-enabled/$DOMAIN" nginx -t && (systemctl reload nginx 2>/dev/null || nginx -s reload) echo "[OK] Server $DOMAIN removed." } # --- CLI router --- CMD="${1:-}" case "$CMD" in turn) install_turn ;; dendrite) install_dendrite ;; synapse) install_synapse ;; list) list_servers ;; remove) remove_server "${2:-}" ;; *) usage ;; esac