Files
ess-docker-compose/deploy.sh
T
wmair e2f578fde5 Fix MAS adminapi listener missing — Element Admin panel broken
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>
2026-03-19 17:16:44 +01:00

1929 lines
67 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# Automated Matrix Stack Deployment Script
# This script handles the complete deployment from scratch
# Supports both local testing and production deployments
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
NC='\033[0m' # No Color
# Use sudo for docker commands
DOCKER_CMD="sudo docker"
DOCKER_COMPOSE_CMD="sudo docker compose"
echo -e "${YELLOW}Using sudo for docker commands.${NC}"
echo ""
# Test docker access
if ! sudo docker ps &> /dev/null; then
echo -e "${RED}Error: Cannot access Docker. Please ensure Docker is running.${NC}"
exit 1
fi
clear
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Matrix Stack Automated Deployment Script ║${NC}"
echo -e "${BLUE}║ Interactive Setup ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""
# ============================================================================
# DEPLOYMENT TYPE SELECTION
# ============================================================================
echo -e "${CYAN}Select Deployment Type:${NC}"
echo ""
echo -e " ${GREEN}1)${NC} Local Testing (All-in-One)"
echo -e " → Everything on one machine with self-signed certificates"
echo -e " → Uses *.localhost domains"
echo -e " → Caddy + Authelia + Matrix stack together"
echo ""
echo -e " ${GREEN}2)${NC} Production (Distributed)"
echo -e " → Services on separate machines for security"
echo -e " → Machine 1: Caddy (SSL termination)"
echo -e " → Machine 2: Authelia (SSO)"
echo -e " → Machine 3: Matrix stack (Synapse, Element, MAS, bridges)"
echo -e " → Real domains with Let's Encrypt certificates"
echo ""
read -p "Enter choice [1 or 2]: " DEPLOYMENT_TYPE
if [[ "$DEPLOYMENT_TYPE" == "1" ]]; then
DEPLOYMENT_MODE="local"
COMPOSE_FILE="compose-variants/docker-compose.local.yml"
# Docker Compose v5+ resolves volume paths relative to the compose file's directory.
# --project-directory . overrides this so all paths resolve from the project root.
DOCKER_COMPOSE_CMD="sudo docker compose --project-directory ."
echo -e "${GREEN}${NC} Selected: Local Testing Mode"
elif [[ "$DEPLOYMENT_TYPE" == "2" ]]; then
DEPLOYMENT_MODE="production"
COMPOSE_FILE="docker-compose.yml"
echo -e "${GREEN}${NC} Selected: Production Mode"
else
echo -e "${RED}${NC} Invalid choice. Exiting."
exit 1
fi
echo ""
# ============================================================================
# DATA DIRECTORY CHECK & AUTOMATIC CLEANUP
# ============================================================================
echo -e "${YELLOW}Checking for existing data directories...${NC}"
EXISTING_DATA=""
PRESERVED_CLIENT_SECRET="" # Will store existing CLIENT_SECRET to preserve Authelia integration
[[ -d "postgres/data" ]] && [[ "$(ls -A postgres/data 2>/dev/null)" ]] && EXISTING_DATA="${EXISTING_DATA}postgres/data "
[[ -d "synapse/data" ]] && [[ -f "synapse/data/homeserver.yaml" ]] && EXISTING_DATA="${EXISTING_DATA}synapse/data "
[[ -d "mas/data" ]] && [[ "$(ls -A mas/data 2>/dev/null)" ]] && EXISTING_DATA="${EXISTING_DATA}mas/data "
if [[ -n "$EXISTING_DATA" ]]; then
echo -e "${RED}⚠ WARNING: Existing data directories found:${NC}"
for dir in $EXISTING_DATA; do
echo -e "$dir"
done
echo ""
echo -e "${YELLOW}Automatically cleaning to prevent password mismatch issues...${NC}"
echo -e "${YELLOW}(Old database passwords won't match new deployment)${NC}"
echo ""
# Extract CLIENT_SECRET before cleanup to preserve Authelia integration
if [[ -f "mas/config/config.yaml" ]]; then
PRESERVED_CLIENT_SECRET=$(grep "client_secret:" mas/config/config.yaml | head -1 | sed "s/.*client_secret: '\(.*\)'/\1/")
if [[ -n "$PRESERVED_CLIENT_SECRET" ]]; then
echo -e "${GREEN}${NC} Found existing Authelia client_secret - will preserve it"
fi
fi
# Stop all containers and remove volumes to prevent PostgreSQL password conflicts
echo -e "${YELLOW}Stopping containers and removing volumes...${NC}"
$DOCKER_COMPOSE_CMD -f ${COMPOSE_FILE} down -v 2>/dev/null || true
# Surgical cleanup: Only remove postgres/data (source of password mismatch)
# Preserve synapse/data/homeserver.yaml (keeps custom mail config, etc.)
# Preserve mas/config (keeps CLIENT_SECRET for Authelia)
echo -e "${YELLOW}Cleaning PostgreSQL data to fix password mismatch...${NC}"
echo -e "${GREEN}${NC} Preserving synapse/data/homeserver.yaml (custom configs maintained)"
echo -e "${GREEN}${NC} Preserving mas/config (Authelia integration maintained)"
# Remove bridge registration references from homeserver.yaml
if [[ -f "synapse/data/homeserver.yaml" ]]; then
sudo sed -i '/^ - \/bridges\//d' synapse/data/homeserver.yaml
echo -e "${GREEN}${NC} Removed stale bridge registration references"
fi
sudo rm -rf postgres/data
sudo rm -rf mas/data mas/certs
sudo rm -rf caddy/data caddy/config
sudo rm -rf bridges/*/config
mkdir -p postgres/data synapse/data mas/data mas/certs caddy/data caddy/config
mkdir -p bridges/telegram/config bridges/whatsapp/config bridges/signal/config
echo -e "${GREEN}${NC} PostgreSQL data cleaned - custom configurations preserved"
echo ""
else
echo -e "${GREEN}${NC} No existing data found - starting with clean slate"
echo ""
fi
# ============================================================================
# AUTHELIA SSO SELECTION
# ============================================================================
echo -e "${CYAN}Include Authelia SSO?${NC}"
echo ""
echo -e " ${GREEN}Yes)${NC} Use Authelia as upstream OAuth provider"
echo -e " → Full SSO with 2FA support"
echo -e " → Users authenticate through Authelia"
echo -e " → Additional authentication layer"
echo ""
echo -e " ${GREEN}No)${NC} MAS handles authentication directly"
echo -e " → Simpler setup, fewer moving parts"
echo -e " → MAS manages users directly"
echo -e " → Password-based authentication"
echo ""
read -p "Include Authelia? [y/N]: " INCLUDE_AUTHELIA
if [[ "$INCLUDE_AUTHELIA" =~ ^[Yy]$ ]]; then
USE_AUTHELIA=true
echo -e "${GREEN}${NC} Authelia SSO will be included"
else
USE_AUTHELIA=false
echo -e "${GREEN}${NC} MAS will handle authentication directly (no Authelia)"
fi
echo ""
# ============================================================================
# ELEMENT CALL SELECTION
# ============================================================================
echo -e "${CYAN}Enable Element Call (video/voice calling)?${NC}"
echo ""
echo -e " ${GREEN}Yes)${NC} Add LiveKit-based video/voice calls"
echo -e " → Adds livekit + lk-jwt-service containers"
echo -e " → Element Web will show video/voice call button"
echo -e " → Requires ports TCP 7881 and UDP 50100-50200 open to the internet"
echo ""
echo -e " ${GREEN}No)${NC} Text-only Matrix stack (default)"
echo ""
read -p "Enable Element Call? [y/N]: " INCLUDE_ELEMENT_CALL
if [[ "$INCLUDE_ELEMENT_CALL" =~ ^[Yy]$ ]]; then
USE_ELEMENT_CALL=true
echo -e "${GREEN}${NC} Element Call will be enabled"
else
USE_ELEMENT_CALL=false
echo -e "${GREEN}${NC} Element Call disabled"
fi
echo ""
# ============================================================================
# DOCKER REGISTRY AND HARDENED IMAGES
# ============================================================================
echo -e "${CYAN}Docker Image Configuration:${NC}"
echo ""
read -p "Custom Docker registry prefix (leave blank for default): " DOCKER_REGISTRY_INPUT
DOCKER_REGISTRY="${DOCKER_REGISTRY_INPUT%/}" # strip trailing slash
[ -n "$DOCKER_REGISTRY" ] && DOCKER_REGISTRY="${DOCKER_REGISTRY}/"
if [ -n "$DOCKER_REGISTRY" ]; then
echo -e "${GREEN}${NC} Custom registry: ${DOCKER_REGISTRY}"
else
echo -e "${GREEN}${NC} Using default registries"
fi
USE_HARDENED_IMAGES=false
read -p "Use hardened images from dhi.io for Redis/PostgreSQL/Caddy? [y/N]: " yn
[[ "$yn" =~ ^[Yy] ]] && USE_HARDENED_IMAGES=true
if [ "$USE_HARDENED_IMAGES" = true ]; then
echo -e "${GREEN}${NC} Hardened images (dhi.io) enabled for Redis/PostgreSQL/Caddy"
else
echo -e "${GREEN}${NC} Using standard images"
fi
echo ""
# Build image reference helper
build_image() {
local image="$1"
if [ -n "$DOCKER_REGISTRY" ]; then
echo "${DOCKER_REGISTRY}${image}"
else
echo "${image}"
fi
}
# Standard images (respect custom registry)
POSTGRES_IMAGE=$(build_image "postgres:16-alpine")
SYNAPSE_IMAGE=$(build_image "matrixdotorg/synapse:latest")
ELEMENT_IMAGE=$(build_image "vectorim/element-web:latest")
ELEMENT_ADMIN_IMAGE=$(build_image "oci.element.io/element-admin:latest")
MAS_IMAGE=$(build_image "ghcr.io/element-hq/matrix-authentication-service:latest")
TELEGRAM_IMAGE=$(build_image "dock.mau.dev/mautrix/telegram:latest")
WHATSAPP_IMAGE=$(build_image "dock.mau.dev/mautrix/whatsapp:latest")
SIGNAL_IMAGE=$(build_image "dock.mau.dev/mautrix/signal:latest")
LIVEKIT_IMAGE=$(build_image "livekit/livekit-server:latest")
LK_JWT_IMAGE=$(build_image "ghcr.io/element-hq/lk-jwt-service:latest")
ELEMENT_CALL_IMAGE=$(build_image "ghcr.io/element-hq/element-call:latest")
AUTHELIA_IMAGE=$(build_image "authelia/authelia:latest")
# Hardened images take priority for redis/postgres/caddy
if [ "$USE_HARDENED_IMAGES" = true ]; then
REDIS_IMAGE="dhi.io/redis:7"
POSTGRES_IMAGE="dhi.io/postgres:16"
CADDY_IMAGE="dhi.io/caddy:2"
else
REDIS_IMAGE=$(build_image "redis:7-alpine")
CADDY_IMAGE=$(build_image "caddy:2-alpine")
fi
# Function to generate secure random string (32 bytes base64)
generate_secret() {
openssl rand -base64 32 | tr -d "=+/" | cut -c1-32
}
# Function to generate secure hex string (for MAS encryption)
generate_hex_secret() {
openssl rand -hex 32
}
# Function to print status
print_status() {
echo -e "${GREEN}${NC} $1"
}
print_error() {
echo -e "${RED}${NC} $1"
}
print_warning() {
echo -e "${YELLOW}${NC} $1"
}
print_info() {
echo -e "${BLUE}${NC} $1"
}
# ============================================================================
# DOMAIN AND CONFIGURATION PROMPTS
# ============================================================================
if [[ "$DEPLOYMENT_MODE" == "local" ]]; then
# Local testing with example.test domains (not .localhost - it's on the public suffix list!)
DOMAIN_BASE="example.test"
AUTHELIA_COOKIE_DOMAIN="example.test" # No leading dot for cookie domain
MATRIX_DOMAIN="matrix.example.test"
ELEMENT_DOMAIN="element.example.test"
ADMIN_DOMAIN="admin.example.test"
AUTH_DOMAIN="auth.example.test"
AUTHELIA_DOMAIN="authelia.example.test"
RTC_DOMAIN="rtc.example.test"
CALL_DOMAIN="call.example.test"
# Matrix server name (MXID identity domain)
echo ""
echo -e "${CYAN}Matrix User ID format:${NC}"
echo -e " [1] Short: @user:${DOMAIN_BASE} ← recommended"
echo -e " [2] Subdomain: @user:${MATRIX_DOMAIN}"
read -p "Choose [1/2, default: 1]: " _sn_choice
if [[ "$_sn_choice" == "2" ]]; then
SERVER_NAME="${MATRIX_DOMAIN}"
else
SERVER_NAME="${DOMAIN_BASE}"
fi
echo -e "${CYAN}Local Testing Configuration:${NC}"
echo -e " Matrix API: https://${MATRIX_DOMAIN}"
echo -e " Element Web: https://${ELEMENT_DOMAIN}"
echo -e " MAS Auth: https://${AUTH_DOMAIN}"
echo -e " Authelia: https://${AUTHELIA_DOMAIN}"
if [[ "$USE_ELEMENT_CALL" == true ]]; then
echo -e " Element Call: https://${CALL_DOMAIN}"
echo -e " LiveKit RTC: https://${RTC_DOMAIN}"
fi
echo ""
HOSTS_DOMAINS="${MATRIX_DOMAIN} ${ELEMENT_DOMAIN} ${AUTH_DOMAIN} ${AUTHELIA_DOMAIN}"
if [[ "$SERVER_NAME" != "$MATRIX_DOMAIN" ]]; then
HOSTS_DOMAINS="${SERVER_NAME} ${HOSTS_DOMAINS}"
fi
if [[ "$USE_ELEMENT_CALL" == true ]]; then
HOSTS_DOMAINS="${HOSTS_DOMAINS} ${RTC_DOMAIN} ${CALL_DOMAIN}"
fi
echo -e "${YELLOW}⚠ Remember to add these to /etc/hosts:${NC}"
echo -e " 127.0.0.1 ${HOSTS_DOMAINS}"
echo -e " ::1 ${HOSTS_DOMAINS}"
echo ""
echo -e "${BLUE} Note: IPv6 entry (::1) required to prevent DNS lookups bypassing /etc/hosts${NC}"
echo ""
read -p "Press Enter to continue..."
echo ""
else
# Production deployment
echo -e "${CYAN}Production Deployment Configuration${NC}"
echo ""
# Base domain
read -p "Enter your base domain (e.g., example.com): " DOMAIN_BASE
AUTHELIA_COOKIE_DOMAIN="${DOMAIN_BASE}" # Production uses the base domain
# Matrix subdomain
read -p "Enter Matrix subdomain [default: matrix]: " MATRIX_SUBDOMAIN
MATRIX_SUBDOMAIN=${MATRIX_SUBDOMAIN:-matrix}
MATRIX_DOMAIN="${MATRIX_SUBDOMAIN}.${DOMAIN_BASE}"
# Element subdomain
read -p "Enter Element subdomain [default: element]: " ELEMENT_SUBDOMAIN
ELEMENT_SUBDOMAIN=${ELEMENT_SUBDOMAIN:-element}
ELEMENT_DOMAIN="${ELEMENT_SUBDOMAIN}.${DOMAIN_BASE}"
# Element Admin subdomain
read -p "Enter Element Admin subdomain [default: admin]: " ADMIN_SUBDOMAIN
ADMIN_SUBDOMAIN=${ADMIN_SUBDOMAIN:-admin}
ADMIN_DOMAIN="${ADMIN_SUBDOMAIN}.${DOMAIN_BASE}"
# MAS subdomain
read -p "Enter MAS/Auth subdomain [default: auth]: " AUTH_SUBDOMAIN
AUTH_SUBDOMAIN=${AUTH_SUBDOMAIN:-auth}
AUTH_DOMAIN="${AUTH_SUBDOMAIN}.${DOMAIN_BASE}"
# Authelia subdomain
read -p "Enter Authelia subdomain [default: authelia]: " AUTHELIA_SUBDOMAIN
AUTHELIA_SUBDOMAIN=${AUTHELIA_SUBDOMAIN:-authelia}
AUTHELIA_DOMAIN="${AUTHELIA_SUBDOMAIN}.${DOMAIN_BASE}"
# RTC subdomain (LiveKit signaling) + Call subdomain (Element Call frontend)
if [[ "$USE_ELEMENT_CALL" == true ]]; then
read -p "Enter RTC subdomain for LiveKit [default: rtc]: " RTC_SUBDOMAIN
RTC_SUBDOMAIN=${RTC_SUBDOMAIN:-rtc}
RTC_DOMAIN="${RTC_SUBDOMAIN}.${DOMAIN_BASE}"
read -p "Enter subdomain for Element Call frontend [default: call]: " CALL_SUBDOMAIN
CALL_SUBDOMAIN=${CALL_SUBDOMAIN:-call}
CALL_DOMAIN="${CALL_SUBDOMAIN}.${DOMAIN_BASE}"
fi
# Matrix server name (MXID identity domain)
echo ""
echo -e "${CYAN}Matrix User ID format:${NC}"
echo -e " [1] Short: @user:${DOMAIN_BASE} ← recommended"
echo -e " [2] Subdomain: @user:${MATRIX_DOMAIN}"
read -p "Choose [1/2, default: 1]: " _sn_choice
if [[ "$_sn_choice" == "2" ]]; then
SERVER_NAME="${MATRIX_DOMAIN}"
else
SERVER_NAME="${DOMAIN_BASE}"
fi
echo ""
echo -e "${CYAN}Backend Server Addresses (for Caddyfile):${NC}"
echo -e " ${YELLOW}Enter IP addresses or hostnames${NC}"
echo ""
# Matrix server address (IP or hostname)
read -p "Matrix server address (IP or hostname): " MATRIX_SERVER_IP
MATRIX_SERVER_IP=${MATRIX_SERVER_IP:-10.0.1.10}
# Authelia server address (IP or hostname)
read -p "Authelia server address (IP or hostname): " AUTHELIA_SERVER_IP
AUTHELIA_SERVER_IP=${AUTHELIA_SERVER_IP:-10.0.1.20}
# Email for Let's Encrypt (used in generated Caddyfile template)
read -p "Email for Let's Encrypt [default: admin@${DOMAIN_BASE}]: " LETSENCRYPT_EMAIL
LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL:-admin@${DOMAIN_BASE}}
echo ""
echo -e "${GREEN}${NC} Configuration Summary:"
echo -e " Base Domain: ${DOMAIN_BASE}"
echo -e " Server Name: ${SERVER_NAME} (@user:${SERVER_NAME})"
echo -e " Matrix: https://${MATRIX_DOMAIN}"
echo -e " Element: https://${ELEMENT_DOMAIN}"
echo -e " MAS: https://${AUTH_DOMAIN}"
echo -e " Authelia: https://${AUTHELIA_DOMAIN}"
if [[ "$USE_ELEMENT_CALL" == true ]]; then
echo -e " Element Call RTC: https://${RTC_DOMAIN}"
fi
echo -e " Matrix Backend: ${MATRIX_SERVER_IP}"
echo -e " Authelia Backend: ${AUTHELIA_SERVER_IP}"
echo ""
print_info "Note: Generated Caddyfile will use these backend addresses"
print_info " Copy generated configs from authelia/config/ to your Authelia server"
echo ""
fi
# Step 1: Check prerequisites
echo -e "${BLUE}[1/13] Checking prerequisites...${NC}"
if ! command -v openssl &> /dev/null; then
print_error "openssl is not installed"
exit 1
fi
if ! $DOCKER_CMD --version &> /dev/null; then
print_error "Docker is not accessible"
exit 1
fi
print_status "Prerequisites OK"
echo ""
# Step 1.5: Create directory structure
echo -e "${BLUE}[1.5/13] Creating directory structure...${NC}"
mkdir -p authelia/config
mkdir -p mas/config mas/data mas/certs
mkdir -p element/config
mkdir -p synapse/data
mkdir -p postgres/data
mkdir -p caddy/data caddy/config
mkdir -p bridges/{telegram,whatsapp,signal}/config
mkdir -p appservices
print_status "Directory structure created"
echo ""
# Step 1.5 (cont.): Create livekit directory if needed
if [[ "$USE_ELEMENT_CALL" == true ]]; then
mkdir -p livekit
fi
# Step 2: Generate secure secrets
echo -e "${BLUE}[2/12] Generating secure secrets...${NC}"
POSTGRES_PASSWORD=$(generate_secret)
AUTHELIA_JWT_SECRET=$(generate_secret)
AUTHELIA_SESSION_SECRET=$(generate_secret)
AUTHELIA_STORAGE_ENCRYPTION_KEY=$(generate_secret)
MAS_SECRET_KEY=$(generate_hex_secret) # MAS requires hex format
SYNAPSE_SHARED_SECRET=$(generate_secret)
DOUBLEPUPPET_AS_TOKEN=$(generate_hex_secret)
DOUBLEPUPPET_HS_TOKEN=$(generate_hex_secret)
if [[ "$USE_ELEMENT_CALL" == true ]]; then
LIVEKIT_SECRET=$(generate_secret)
fi
print_status "Secrets generated"
echo ""
# Step 3: Update .env file
echo -e "${BLUE}[3/13] Updating .env file with generated secrets...${NC}"
cat > .env << EOF
# Matrix Stack Environment Variables
# Auto-generated by deploy.sh on $(date)
# Deployment Mode: ${DEPLOYMENT_MODE}
# Domain Configuration
DOMAIN_BASE=${DOMAIN_BASE}
MATRIX_DOMAIN=${MATRIX_DOMAIN}
ELEMENT_DOMAIN=${ELEMENT_DOMAIN}
ADMIN_DOMAIN=${ADMIN_DOMAIN}
AUTH_DOMAIN=${AUTH_DOMAIN}
AUTHELIA_DOMAIN=${AUTHELIA_DOMAIN}
SERVER_NAME=${SERVER_NAME}
# PostgreSQL
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
# Synapse
SYNAPSE_REPORT_STATS=no
SYNAPSE_SHARED_SECRET=${SYNAPSE_SHARED_SECRET}
# Authelia
AUTHELIA_JWT_SECRET=${AUTHELIA_JWT_SECRET}
AUTHELIA_SESSION_SECRET=${AUTHELIA_SESSION_SECRET}
AUTHELIA_STORAGE_ENCRYPTION_KEY=${AUTHELIA_STORAGE_ENCRYPTION_KEY}
AUTHELIA_POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
# MAS
MAS_DATABASE_URL=postgresql://synapse:${POSTGRES_PASSWORD}@postgres/mas
MAS_SECRET_KEY=${MAS_SECRET_KEY}
# Timezone
TZ=${TZ:-Europe/Berlin}
# Docker images
POSTGRES_IMAGE=${POSTGRES_IMAGE}
SYNAPSE_IMAGE=${SYNAPSE_IMAGE}
ELEMENT_IMAGE=${ELEMENT_IMAGE}
ELEMENT_ADMIN_IMAGE=${ELEMENT_ADMIN_IMAGE}
REDIS_IMAGE=${REDIS_IMAGE}
MAS_IMAGE=${MAS_IMAGE}
TELEGRAM_IMAGE=${TELEGRAM_IMAGE}
WHATSAPP_IMAGE=${WHATSAPP_IMAGE}
SIGNAL_IMAGE=${SIGNAL_IMAGE}
LIVEKIT_IMAGE=${LIVEKIT_IMAGE}
LK_JWT_IMAGE=${LK_JWT_IMAGE}
ELEMENT_CALL_IMAGE=${ELEMENT_CALL_IMAGE}
AUTHELIA_IMAGE=${AUTHELIA_IMAGE}
CADDY_IMAGE=${CADDY_IMAGE}
EOF
# Add production-specific variables
if [[ "$DEPLOYMENT_MODE" == "production" ]]; then
cat >> .env << EOF
# Production Configuration (Backend addresses for Caddyfile generation)
# These are used in the generated caddy/Caddyfile.production template
MATRIX_SERVER_IP=${MATRIX_SERVER_IP}
AUTHELIA_SERVER_IP=${AUTHELIA_SERVER_IP}
LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
EOF
fi
# Add Element Call variables
if [[ "$USE_ELEMENT_CALL" == true ]]; then
cat >> .env << EOF
# Element Call (LiveKit + self-hosted frontend)
RTC_DOMAIN=${RTC_DOMAIN}
CALL_DOMAIN=${CALL_DOMAIN}
LIVEKIT_SECRET=${LIVEKIT_SECRET}
EOF
fi
# Telegram bridge credentials placeholder (obtain from https://my.telegram.org)
if ! grep -q "TELEGRAM_API_ID" .env 2>/dev/null; then
cat >> .env << 'ENVEOF'
# Telegram Bridge — obtain API credentials at https://my.telegram.org (Apps tab)
# Uncomment and fill in before running setup-bridges.sh to enable Telegram
# TELEGRAM_API_ID=your_api_id
# TELEGRAM_API_HASH=your_api_hash
ENVEOF
fi
print_status ".env file updated"
echo ""
# Conditional: Authelia configuration (Steps 4-8)
if [[ "$USE_AUTHELIA" == true ]]; then
# Step 4: Generate RSA key for Authelia
echo -e "${BLUE}[4/13] Generating RSA key for Authelia OIDC...${NC}"
openssl genrsa -out authelia_private.pem 4096 2>/dev/null
AUTHELIA_RSA_KEY=$(cat authelia_private.pem)
print_status "Authelia RSA key generated"
echo ""
# Step 5: Generate or reuse client secret for Authelia
echo -e "${BLUE}[5/13] Configuring Authelia client secret...${NC}"
if [[ -n "$PRESERVED_CLIENT_SECRET" ]]; then
# Reuse preserved secret to maintain Authelia integration
CLIENT_SECRET_PLAIN="$PRESERVED_CLIENT_SECRET"
CLIENT_SECRET_HASH=$($DOCKER_CMD run --rm authelia/authelia:latest authelia crypto hash generate pbkdf2 --variant sha512 --password "${CLIENT_SECRET_PLAIN}" 2>/dev/null | grep "Digest:" | awk '{print $2}')
print_status "Reusing preserved client secret (Authelia integration maintained)"
else
# Generate new secret
CLIENT_SECRET_PLAIN=$(generate_secret)
CLIENT_SECRET_HASH=$($DOCKER_CMD run --rm authelia/authelia:latest authelia crypto hash generate pbkdf2 --variant sha512 --password "${CLIENT_SECRET_PLAIN}" 2>/dev/null | grep "Digest:" | awk '{print $2}')
print_status "Client secret generated"
fi
echo ""
# Step 6: Generate password hash for default admin user
echo -e "${BLUE}[6/13] Generating default admin user...${NC}"
ADMIN_PASSWORD=$(generate_secret) # Generate secure random password
ADMIN_PASSWORD_HASH=$($DOCKER_CMD run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password "${ADMIN_PASSWORD}" 2>/dev/null | grep "Digest:" | awk '{print $2}')
print_status "Admin user password hash generated"
print_warning "Default admin password: ${ADMIN_PASSWORD} (SAVE THIS - you'll need it to log in!)"
echo ""
# Step 7: Update Authelia configuration
echo -e "${BLUE}[7/12] Configuring Authelia...${NC}"
cat > authelia/config/configuration.yml << EOF
---
# Authelia Configuration for Matrix Stack
theme: auto
server:
address: 'tcp://0.0.0.0:9091'
log:
level: 'info'
format: 'text'
authentication_backend:
file:
path: '/config/users_database.yml'
password:
algorithm: 'argon2'
argon2:
variant: 'argon2id'
iterations: 3
memory: 65536
parallelism: 4
key_length: 32
salt_length: 16
session:
secret: '${AUTHELIA_SESSION_SECRET}'
cookies:
- domain: '${AUTHELIA_COOKIE_DOMAIN}'
authelia_url: 'https://${AUTHELIA_DOMAIN}'
default_redirection_url: 'https://${ELEMENT_DOMAIN}'
redis:
host: 'redis'
port: 6379
storage:
encryption_key: '${AUTHELIA_STORAGE_ENCRYPTION_KEY}'
postgres:
address: 'tcp://postgres:5432'
database: 'authelia'
username: 'synapse'
password: '${POSTGRES_PASSWORD}'
notifier:
filesystem:
filename: '/config/notification.txt'
identity_validation:
reset_password:
jwt_secret: '${AUTHELIA_JWT_SECRET}'
access_control:
default_policy: 'deny'
rules:
- domain:
- 'matrix.localhost'
policy: 'two_factor'
- domain:
- 'element.matrix.localhost'
policy: 'two_factor'
identity_providers:
oidc:
hmac_secret: '${AUTHELIA_JWT_SECRET}'
jwks:
- key_id: 'main'
algorithm: 'RS256'
use: 'sig'
key: |
EOF
# Add the RSA key with proper indentation
echo "$AUTHELIA_RSA_KEY" | sed 's/^/ /' >> authelia/config/configuration.yml
# Continue with the rest of the config
cat >> authelia/config/configuration.yml << EOF
clients:
- client_id: 'mas-client'
client_name: 'Matrix Authentication Service'
client_secret: '${CLIENT_SECRET_HASH}'
public: false
authorization_policy: 'one_factor' # Change to two_factor in production!
redirect_uris:
- 'https://${AUTH_DOMAIN}/callback'
- 'https://${AUTH_DOMAIN}/oauth2/callback'
- 'https://${AUTH_DOMAIN}/upstream/callback/01HQW90Z35CMXFJWQPHC3BGZGQ' # MAS upstream callback
scopes:
- 'openid'
- 'profile'
- 'email'
- 'offline_access'
grant_types:
- 'authorization_code'
- 'refresh_token'
response_types:
- 'code'
token_endpoint_auth_method: 'client_secret_basic'
EOF
print_status "Authelia configuration updated"
echo ""
# Step 8: Create Authelia users database
echo -e "${BLUE}[8/13] Creating Authelia users database...${NC}"
cat > authelia/config/users_database.yml << EOF
---
# Authelia Users Database
users:
admin:
displayname: "Admin User"
password: "${ADMIN_PASSWORD_HASH}"
email: admin@${MATRIX_DOMAIN}
groups:
- admins
- users
EOF
print_status "Authelia users database created"
echo ""
else
print_info "Skipping Authelia configuration (not included in deployment)"
echo ""
fi
# Step 9: Generate MAS signing key and Synapse client secret
echo -e "${BLUE}[9/12] Generating MAS signing key and Synapse client secret...${NC}"
openssl genrsa 4096 2>/dev/null | openssl pkcs8 -topk8 -nocrypt > mas-signing.key 2>/dev/null
MAS_SIGNING_KEY=$(cat mas-signing.key)
SYNAPSE_CLIENT_SECRET=$(generate_secret)
print_status "MAS signing key and Synapse client secret generated"
echo ""
# Step 10: Configure MAS
echo -e "${BLUE}[10/12] Configuring MAS...${NC}"
cat > mas/config/config.yaml << EOF
---
# Matrix Authentication Service (MAS) Configuration
http:
listeners:
- name: web
resources:
- name: discovery
- name: human
- name: oauth
- name: compat
- name: graphql
playground: true
- name: assets # Required for CSS/JS files
- 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
# Add the MAS signing key with proper indentation
echo "$MAS_SIGNING_KEY" | sed 's/^/ /' >> mas/config/config.yaml
# Continue with the rest of the MAS config - conditional based on Authelia usage
if [[ "$USE_AUTHELIA" == true ]]; then
# With Authelia: Use upstream OAuth2 provider
# Set discovery URL based on deployment mode
if [[ "$DEPLOYMENT_MODE" == "production" ]]; then
AUTHELIA_DISCOVERY_URL="https://${AUTHELIA_DOMAIN}/.well-known/openid-configuration"
else
# Local: Use internal HTTP to avoid self-signed cert issues between containers
AUTHELIA_DISCOVERY_URL="http://authelia:9091/.well-known/openid-configuration"
fi
cat >> mas/config/config.yaml << EOF
upstream_oauth2:
providers:
- id: '01HQW90Z35CMXFJWQPHC3BGZGQ'
issuer: 'https://${AUTHELIA_DOMAIN}'
discovery_url: '${AUTHELIA_DISCOVERY_URL}'
client_id: 'mas-client'
client_secret: '${CLIENT_SECRET_PLAIN}'
scope: 'openid profile email offline_access'
token_endpoint_auth_method: 'client_secret_basic'
fetch_userinfo: true # Critical: Must fetch userinfo for Authelia claims
claims_imports:
localpart:
action: force
template: '{{ user.preferred_username }}' # Works with Authelia
displayname:
action: suggest
template: '{{ user.preferred_username }}' # Authelia provides preferred_username, not name
email:
action: force
template: '{{ user.email }}'
set_email_verification: always
matrix:
homeserver: '${SERVER_NAME}'
endpoint: 'http://synapse:8008'
secret: '${SYNAPSE_SHARED_SECRET}'
passwords:
enabled: false # Using Authelia SSO instead
EOF
else
# Without Authelia: MAS handles authentication directly
cat >> mas/config/config.yaml << EOF
matrix:
homeserver: '${SERVER_NAME}'
endpoint: 'http://synapse:8008'
secret: '${SYNAPSE_SHARED_SECRET}'
passwords:
enabled: true # MAS handles password authentication directly
EOF
fi
# Common configuration continues (email, branding, policy, clients)
cat >> mas/config/config.yaml << EOF
email:
from: '"Matrix Authentication Service" <noreply@matrix.localhost>'
reply_to: '"Matrix Support" <support@matrix.localhost>'
transport: smtp
hostname: 'localhost'
port: 25
mode: plain
branding:
service_name: 'Matrix'
policy_uri: 'https://${AUTH_DOMAIN}/privacy'
tos_uri: 'https://${AUTH_DOMAIN}/terms'
policy:
registration:
enabled: true
require_email: true
clients:
# Element Web client (public)
- client_id: '01HQW90Z35CMXFJWQPHC3BGZGQ'
client_auth_method: none
redirect_uris:
- 'https://${ELEMENT_DOMAIN}'
- 'https://${ELEMENT_DOMAIN}/mobile_guide/'
- 'io.element.app:/callback'
# Element Admin (public - for admin UI)
- client_id: '01ADMN00000000000000000000'
client_auth_method: none
redirect_uris:
- 'https://${ADMIN_DOMAIN}/'
- 'https://${ADMIN_DOMAIN}'
# Synapse client (confidential - for backend integration)
- client_id: '0000000000000000000SYNAPSE'
client_auth_method: client_secret_basic
client_secret: '${SYNAPSE_CLIENT_SECRET}'
EOF
if [[ "$USE_AUTHELIA" == true ]]; then
print_status "MAS configuration created (with Authelia upstream provider)"
else
print_status "MAS configuration created (password authentication enabled)"
fi
echo ""
# Step 11: Create Element Web configuration
echo -e "${BLUE}[11/13] Creating Element Web configuration...${NC}"
# Build Element Call block if enabled
if [[ "$USE_ELEMENT_CALL" == true ]]; 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": "${SERVER_NAME}"
}
},
"brand": "Element",
"integrations_ui_url": "https://scalar.vector.im/",
"integrations_rest_url": "https://scalar.vector.im/api",
"integrations_widgets_urls": [
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api"
],
"show_labs_settings": true,
"piwik": false,
"room_directory": {
"servers": [
"matrix.org",
"${MATRIX_DOMAIN}"
]
},
"features": {
"feature_oidc_aware_navigation": true${ELEMENT_CALL_FEATURES}
},
"default_server_name": "${SERVER_NAME}",
"disable_custom_urls": false,
"disable_guests": true${ELEMENT_CALL_BLOCK}
}
EOF
print_status "Element Web configuration created"
echo ""
# Step 11.5: Create LiveKit config (if Element Call enabled)
if [[ "$USE_ELEMENT_CALL" == true ]]; then
echo -e "${BLUE}[11.5/13] Creating LiveKit configuration...${NC}"
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
print_status "LiveKit configuration created"
echo ""
fi
# Step 12: Generate Synapse configuration
echo -e "${BLUE}[12/13] Generating Synapse configuration...${NC}"
# Generate homeserver.yaml if it doesn't exist
if [ ! -f "synapse/data/homeserver.yaml" ]; then
print_info "Generating new homeserver.yaml..."
$DOCKER_CMD run --rm \
-v $(pwd)/synapse/data:/data \
-e SYNAPSE_SERVER_NAME=${SERVER_NAME} \
-e SYNAPSE_REPORT_STATS=no \
matrixdotorg/synapse:latest generate
# Fix ownership: synapse runs as uid 991 inside Docker; reclaim files for current user
sudo chown -R "$(id -u):$(id -g)" synapse/data/
print_status "Synapse configuration generated"
else
print_info "Existing homeserver.yaml found - preserving custom configurations"
fi
# Always update database section (to match new .env password)
print_info "Updating database configuration..."
# Backup before modifying
cp synapse/data/homeserver.yaml synapse/data/homeserver.yaml.bak 2>/dev/null || true
# Remove old database configuration (both SQLite and PostgreSQL)
sed -i '/^database:/,/^[^ ]/{ /^database:/d; /^[^ ]/!d }' synapse/data/homeserver.yaml
# Remove old MAS integration sections (msc3861 removed in Synapse 1.137+)
sed -i '/^# MAS Integration/,/^[^ ]/{ /^# MAS Integration/d; /^[^ ]/!d }' synapse/data/homeserver.yaml
sed -i '/^experimental_features:/,/^[^ ]/{ /^experimental_features:/d; /^[^ ]/!d }' synapse/data/homeserver.yaml
# Remove old stable MAS config block (so we can re-add with correct secret)
sed -i '/^matrix_authentication_service:/,/^[^ ]/{ /^matrix_authentication_service:/d; /^[^ ]/!d }' synapse/data/homeserver.yaml
# Remove stale MAS-related comment lines
sed -i '/^# Matrix Authentication Service (MAS) integration/d' synapse/data/homeserver.yaml
sed -i '/^# Replaces deprecated experimental_features/d' synapse/data/homeserver.yaml
sed -i '/^# Experimental features$/d' synapse/data/homeserver.yaml
# Remove old enable_registration if present
sed -i '/^# Enable registration/d' synapse/data/homeserver.yaml
sed -i '/^enable_registration:/d' synapse/data/homeserver.yaml
# Remove old double-puppeting appservice registration if present (prevents duplication on re-run)
sed -i '/^# Double-puppeting appservice/d' synapse/data/homeserver.yaml
sed -i '/^app_service_config_files:/,/^[^ ]/{ /^app_service_config_files:/d; /^[^ ]/!d }' synapse/data/homeserver.yaml
# Remove old Element Call rate limit config if present (prevents duplication on re-run)
sed -i '/^# Element Call: delayed event rate limiting/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
# Add PostgreSQL and MAS config (always with current passwords)
cat >> synapse/data/homeserver.yaml << EOF
# PostgreSQL Database Configuration (added/updated by deploy.sh)
database:
name: psycopg2
args:
user: synapse
password: ${POSTGRES_PASSWORD}
database: synapse
host: postgres
port: 5432
cp_min: 5
cp_max: 10
# Enable registration (disabled when using MAS/OAuth delegation)
enable_registration: false
# MAS Integration (Synapse 1.136+ stable config — replaces deprecated experimental_features.msc3861)
matrix_authentication_service:
enabled: true
endpoint: 'http://mas:8080'
secret: '${SYNAPSE_SHARED_SECRET}'
EOF
# Add Element Call MSC features if enabled (separate from MAS config)
if [[ "$USE_ELEMENT_CALL" == true ]]; then
cat >> synapse/data/homeserver.yaml << EOF
# Element Call MSC features
experimental_features:
msc3266_enabled: true
msc4222_enabled: true
msc4140_enabled: true
# Element Call: delayed event rate limiting
max_event_delay_duration: 24h
rc_delayed_event_mgmt:
per_second: 1
burst_count: 20
EOF
fi
# Generate double-puppeting appservice registration
cat > appservices/doublepuppet.yaml << EOF
id: doublepuppet
url: null
as_token: "${DOUBLEPUPPET_AS_TOKEN}"
hs_token: "${DOUBLEPUPPET_HS_TOKEN}"
sender_localpart: doublepuppet
rate_limited: false
namespaces:
users:
- regex: "@.*:${SERVER_NAME}"
exclusive: false
EOF
cat >> synapse/data/homeserver.yaml << EOF
# Double-puppeting appservice
app_service_config_files:
- /appservices/doublepuppet.yaml
EOF
# Restore ownership to Synapse uid so the container can read/write its own data
sudo chown -R 991:991 synapse/data/
print_status "Database configuration updated with current credentials"
echo ""
# Step 12.5: Generate local Caddyfile (only for local mode)
if [[ "$DEPLOYMENT_MODE" == "local" ]]; then
echo -e "${BLUE}[12.5/13] Generating local Caddyfile...${NC}"
# Pre-build JSON blobs for the local Caddyfile (single-line, no literal \n)
if [[ "$USE_ELEMENT_CALL" == true ]]; then
LOCAL_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\"}]}"
LOCAL_ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${SERVER_NAME}\"}},\"default_server_name\":\"${SERVER_NAME}\",\"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
LOCAL_WELLKNOWN_JSON="{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\"},\"m.authentication\":{\"issuer\":\"https://${AUTH_DOMAIN}/\"}}"
LOCAL_ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${SERVER_NAME}\"}},\"default_server_name\":\"${SERVER_NAME}\",\"disable_custom_urls\":false,\"disable_guests\":true,\"features\":{\"feature_oidc_aware_navigation\":true}}"
fi
cat > caddy/Caddyfile << 'CADDYEOF'
# Local Development Caddyfile for Matrix Stack
# Uses self-signed certificates for local HTTPS testing
# Auto-generated by deploy.sh — do not edit manually
{
# Use local CA for self-signed certificates
local_certs
# Enable admin API
admin 0.0.0.0:2019
}
CADDYEOF
# Append Matrix homeserver block (variables expanded)
cat >> caddy/Caddyfile << EOF
# =========================
# Matrix Homeserver (Synapse)
# =========================
${MATRIX_DOMAIN}:443 {
# TLS with self-signed cert
tls internal
# Well-known client endpoint
@wk path /.well-known/matrix/client
handle @wk {
header Content-Type application/json
header Access-Control-Allow-Origin "*"
respond \`${LOCAL_WELLKNOWN_JSON}\` 200
}
# Well-known server endpoint (federation)
@wk_server path /.well-known/matrix/server
handle @wk_server {
header Content-Type application/json
respond \`{"m.server":"${MATRIX_DOMAIN}:443"}\` 200
}
# Rendezvous endpoints for QR code login (MSC4108)
@rendezvous path_regexp rendezvous ^/_matrix/client/(unstable|v1)/org\.matrix\.(msc3886|msc4108)/rendezvous.*\$
handle @rendezvous {
header Access-Control-Allow-Origin "*"
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept, If-Match, If-None-Match"
header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
encode {
}
reverse_proxy synapse:8008 {
header_down -Access-Control-Allow-Origin
header_down -Access-Control-Allow-Methods
header_down -Access-Control-Allow-Headers
header_down -Vary
}
}
# Client versions endpoint with CORS
@versions path /_matrix/client/versions
handle @versions {
header Access-Control-Allow-Origin "*"
header Access-Control-Allow-Methods "GET, OPTIONS"
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
reverse_proxy synapse:8008 {
header_down -Access-Control-Allow-Origin
header_down -Access-Control-Allow-Methods
header_down -Access-Control-Allow-Headers
header_down -Vary
}
}
# CORS preflight for auth metadata
@auth_preflight {
method OPTIONS
path /_matrix/client/unstable/org.matrix.msc2965/auth_metadata
}
handle @auth_preflight {
header Access-Control-Allow-Origin "*"
header Access-Control-Allow-Methods "GET, OPTIONS"
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
header Access-Control-Max-Age "86400"
respond 204
}
# CORS preflight for all Matrix API
@preflight {
method OPTIONS
path_regexp matrix ^/_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
}
# MAS compat endpoints (login/logout/refresh) with CORS
@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 "*"
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
reverse_proxy mas:8080 {
header_down -Access-Control-Allow-Origin
header_down -Access-Control-Allow-Methods
header_down -Access-Control-Allow-Headers
header_down -Vary
}
}
# Everything else under /_matrix → Synapse with CORS
@matrix_rest path_regexp matrix ^/_matrix/.*\$
handle @matrix_rest {
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 Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
reverse_proxy synapse:8008 {
header_down -Access-Control-Allow-Origin
header_down -Access-Control-Allow-Methods
header_down -Access-Control-Allow-Headers
header_down -Vary
}
}
# Default: everything else → Synapse
handle {
reverse_proxy synapse:8008
}
}
# =========================
# Matrix Authentication Service (MAS)
# =========================
${AUTH_DOMAIN}:443 {
tls internal
# OIDC Discovery
@disco path /.well-known/openid-configuration
handle @disco {
header ?Access-Control-Allow-Origin "*"
header ?Access-Control-Allow-Methods "GET, OPTIONS"
header ?Access-Control-Allow-Headers "*"
reverse_proxy mas:8080
}
# Dynamic Client Registration: CORS preflight
@reg_opts {
method OPTIONS
path /oauth2/registration
}
handle @reg_opts {
header ?Access-Control-Allow-Origin "*"
header ?Access-Control-Allow-Methods "POST, OPTIONS"
header ?Access-Control-Allow-Headers "*"
respond 204
}
# Dynamic Client Registration (POST)
@reg path /oauth2/registration
route @reg {
header ?Access-Control-Allow-Origin "*"
header ?Access-Control-Allow-Methods "POST, OPTIONS"
header ?Access-Control-Allow-Headers "*"
reverse_proxy mas:8080
}
# JWKS preflight
@jwks_opts {
method OPTIONS
path /oauth2/keys.json
}
handle @jwks_opts {
header ?Access-Control-Allow-Origin "*"
header ?Access-Control-Allow-Methods "GET, OPTIONS"
header ?Access-Control-Allow-Headers "*"
respond 204
}
# Map keys.json → /oauth2/jwks (MAS naming)
@jwksjson path /oauth2/keys.json
route @jwksjson {
header ?Access-Control-Allow-Origin "*"
header ?Access-Control-Allow-Methods "GET, OPTIONS"
header ?Access-Control-Allow-Headers "*"
uri replace /oauth2/keys.json /oauth2/jwks
reverse_proxy mas:8080
}
# Generic OAuth2 endpoints
@oauth path /oauth2/*
route @oauth {
header ?Access-Control-Allow-Origin "*"
header ?Access-Control-Allow-Methods "GET, OPTIONS, POST"
header ?Access-Control-Allow-Headers "*"
reverse_proxy mas:8080
}
# Account portal
handle_path /account/* {
reverse_proxy mas:8080
}
# Authelia endpoints (proxy to authelia)
handle_path /authelia/* {
reverse_proxy authelia:9091
}
# Fallback: everything else to MAS
handle {
reverse_proxy mas:8080
}
# Add CORS on error responses
handle_errors {
header ?Access-Control-Allow-Origin "*"
header ?Access-Control-Allow-Headers "*"
header ?Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
}
}
# =========================
# Authelia SSO
# =========================
${AUTHELIA_DOMAIN}:443 {
tls internal
reverse_proxy authelia:9091
}
# =========================
# Element Web Client
# =========================
${ELEMENT_DOMAIN}:443 {
tls internal
# Serve config.json with proper settings
@cfg path /config.json
handle @cfg {
header Content-Type application/json
header Cache-Control no-store
respond \`${LOCAL_ELEMENT_CFG_JSON}\` 200
}
# Everything else to Element container
handle {
reverse_proxy element:80
}
}
# =========================
# Element Admin
# =========================
${ADMIN_DOMAIN}:443 {
tls internal
handle {
reverse_proxy element-admin:8080
}
}
EOF
# Append identity domain well-known block if SERVER_NAME differs from MATRIX_DOMAIN
if [[ "$SERVER_NAME" != "$MATRIX_DOMAIN" ]]; then
cat >> caddy/Caddyfile << EOF
# =========================
# Identity Domain (well-known delegation)
# =========================
${SERVER_NAME}:443 {
tls internal
@wk path /.well-known/matrix/client
handle @wk {
header Content-Type application/json
header Access-Control-Allow-Origin "*"
respond \`${LOCAL_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
}
}
EOF
fi
# Append Element Call blocks if enabled
if [[ "$USE_ELEMENT_CALL" == true ]]; then
cat >> caddy/Caddyfile << EOF
# =========================
# Element Call (LiveKit)
# =========================
${RTC_DOMAIN}:443 {
tls internal
handle_path /livekit/jwt* {
reverse_proxy lk-jwt-service:8080
}
handle_path /livekit/sfu* {
reverse_proxy livekit:7880
}
}
# =========================
# Element Call Frontend
# =========================
${CALL_DOMAIN}:443 {
tls internal
reverse_proxy element-call:8080
}
EOF
fi
print_status "Local Caddyfile generated: caddy/Caddyfile"
echo ""
fi
# Step 13: Fix directory permissions
echo -e "${BLUE}[13/14] Fixing directory permissions...${NC}"
chmod 755 postgres/init postgres/config 2>/dev/null || true
chmod 644 postgres/init/*.sql 2>/dev/null || true
chmod 755 authelia/config mas/config element/config 2>/dev/null || true
print_status "Permissions fixed"
echo ""
# Image summary
echo -e "${CYAN}Docker images configured:${NC}"
echo -e " Synapse: $SYNAPSE_IMAGE"
echo -e " Postgres: $POSTGRES_IMAGE"
echo -e " Redis: $REDIS_IMAGE"
echo -e " MAS: $MAS_IMAGE"
echo -e " Element: $ELEMENT_IMAGE"
echo -e " Element Admin: $ELEMENT_ADMIN_IMAGE"
echo -e " Authelia: $AUTHELIA_IMAGE"
echo -e " Caddy: $CADDY_IMAGE"
if [[ "$USE_ELEMENT_CALL" == true ]]; then
echo -e " LiveKit: $LIVEKIT_IMAGE"
echo -e " LK JWT: $LK_JWT_IMAGE"
echo -e " Element Call: $ELEMENT_CALL_IMAGE"
fi
echo ""
# Step 14: Start the stack
echo -e "${BLUE}[14/14] Starting the Matrix stack...${NC}"
print_info "Using compose file: ${COMPOSE_FILE}"
print_info "This may take a few minutes on first run..."
echo ""
# Start PostgreSQL first
print_info "Starting PostgreSQL..."
$DOCKER_COMPOSE_CMD -f ${COMPOSE_FILE} up -d postgres
sleep 10
# Wait for PostgreSQL to be ready
print_info "Waiting for PostgreSQL to be ready..."
for i in {1..60}; do
if $DOCKER_COMPOSE_CMD -f ${COMPOSE_FILE} exec -T postgres pg_isready -U synapse &> /dev/null; then
print_status "PostgreSQL is ready"
break
fi
if [ $i -eq 60 ]; then
print_error "PostgreSQL failed to start in time"
echo "Checking logs..."
$DOCKER_COMPOSE_CMD -f ${COMPOSE_FILE} logs postgres | tail -20
exit 1
fi
sleep 2
done
echo ""
# Start Redis (only if using Authelia)
if [[ "$USE_AUTHELIA" == true ]]; then
print_info "Starting Redis (for Authelia)..."
$DOCKER_COMPOSE_CMD -f ${COMPOSE_FILE} --profile authelia up -d redis
sleep 3
echo ""
fi
# Build compose profile flags
COMPOSE_PROFILES=""
if [[ "$USE_AUTHELIA" == true ]]; then
COMPOSE_PROFILES="${COMPOSE_PROFILES} --profile authelia"
fi
if [[ "$USE_ELEMENT_CALL" == true ]]; then
COMPOSE_PROFILES="${COMPOSE_PROFILES} --profile element-call"
fi
# Start remaining services
if [[ -n "$COMPOSE_PROFILES" ]]; then
print_info "Starting all services (profiles:${COMPOSE_PROFILES})..."
$DOCKER_COMPOSE_CMD -f ${COMPOSE_FILE} ${COMPOSE_PROFILES} up -d
else
print_info "Starting all services..."
$DOCKER_COMPOSE_CMD -f ${COMPOSE_FILE} up -d
fi
echo ""
# Wait for services to be ready
print_info "Waiting for services to be ready..."
sleep 10
echo ""
# ============================================================================
# LOCAL: Extract Caddy CA Certificate for MAS
# ============================================================================
if [[ "$DEPLOYMENT_MODE" == "local" ]]; then
echo -e "${BLUE}[Post-Deployment] Extracting Caddy CA certificate for MAS...${NC}"
# Create certs and caddy data directories
mkdir -p mas/certs
mkdir -p caddy/data/caddy # Required for Caddy to save PKI certificates
# Wait for Caddy to generate CA
print_info "Waiting for Caddy to generate local CA..."
sleep 5
# Trigger HTTPS requests to force Caddy to generate certificates
print_info "Triggering certificate generation..."
curl -k https://${AUTH_DOMAIN} > /dev/null 2>&1 || true
sleep 3
# Copy CA certificate from host path (Caddy saves to volume)
if [ -f "caddy/data/caddy/pki/authorities/local/root.crt" ]; then
cp caddy/data/caddy/pki/authorities/local/root.crt mas/certs/caddy-ca.crt
chmod 644 mas/certs/caddy-ca.crt
print_status "Caddy CA certificate copied to mas/certs/caddy-ca.crt"
# Restart MAS to pick up the certificate
print_info "Restarting MAS to load CA certificate..."
$DOCKER_COMPOSE_CMD -f ${COMPOSE_FILE} restart mas
sleep 5
print_status "MAS restarted with trusted CA certificate"
else
print_warning "Could not find Caddy CA certificate at caddy/data/caddy/pki/authorities/local/root.crt"
print_info "You may need to manually copy it after Caddy generates it"
print_info "Run: cp caddy/data/caddy/pki/authorities/local/root.crt mas/certs/caddy-ca.crt"
print_info "Then restart MAS: $DOCKER_COMPOSE_CMD -f ${COMPOSE_FILE} restart mas"
fi
echo ""
fi
# ============================================================================
# PRODUCTION: Generate Caddy and Authelia configs for separate machines
# ============================================================================
if [[ "$DEPLOYMENT_MODE" == "production" ]]; then
echo ""
echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}"
echo -e "${MAGENTA}Generating Production Deployment Configs...${NC}"
echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}"
echo ""
# Generate production Caddyfile
print_info "Generating Caddyfile for Caddy machine..."
# Pre-build conditional JSON blobs for the Caddyfile
if [[ "$USE_ELEMENT_CALL" == true ]]; then
PROD_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\"}]}"
PROD_ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${SERVER_NAME}\"}},\"default_server_name\":\"${SERVER_NAME}\",\"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
PROD_WELLKNOWN_JSON="{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\"},\"m.authentication\":{\"issuer\":\"https://${AUTH_DOMAIN}/\"}}"
PROD_ELEMENT_CFG_JSON="{\"default_server_config\":{\"m.homeserver\":{\"base_url\":\"https://${MATRIX_DOMAIN}\",\"server_name\":\"${SERVER_NAME}\"}},\"default_server_name\":\"${SERVER_NAME}\",\"disable_custom_urls\":false,\"disable_guests\":true,\"features\":{\"feature_oidc_aware_navigation\":true}}"
fi
cat > caddy/Caddyfile.production << EOF
# Production Caddyfile for Matrix Stack
# Deploy this on your SSL termination machine
# Email for Let's Encrypt: ${LETSENCRYPT_EMAIL}
{
email ${LETSENCRYPT_EMAIL}
# Enable admin API (restrict access in firewall)
admin 0.0.0.0:2019
}
# =========================
# Matrix Homeserver
# =========================
${MATRIX_DOMAIN} {
# Well-known client endpoint
@wk path /.well-known/matrix/client
handle @wk {
header Content-Type application/json
respond \`${PROD_WELLKNOWN_JSON}\` 200
}
# Well-known server endpoint (federation)
@wk_server path /.well-known/matrix/server
handle @wk_server {
header Content-Type application/json
respond \`{"m.server":"${MATRIX_DOMAIN}:443"}\` 200
}
# Client versions with CORS
@versions path /_matrix/client/versions
handle @versions {
header Access-Control-Allow-Origin "*"
header Access-Control-Allow-Methods "GET, OPTIONS"
header Access-Control-Allow-Headers "Authorization, Content-Type, Accept"
reverse_proxy ${MATRIX_SERVER_IP}:8008 {
header_down -Access-Control-Allow-Origin
header_down -Access-Control-Allow-Methods
header_down -Access-Control-Allow-Headers
header_down -Vary
}
}
# CORS preflight
@preflight {
method OPTIONS
path_regexp matrix ^/_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
}
# MAS compat endpoints
@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 ${MATRIX_SERVER_IP}:8080 {
header_down -Access-Control-Allow-Origin
header_down -Access-Control-Allow-Methods
header_down -Access-Control-Allow-Headers
header_down -Vary
}
}
# Everything else to Synapse
@matrix_rest path_regexp matrix ^/_matrix/.*$
handle @matrix_rest {
header Access-Control-Allow-Origin "*"
reverse_proxy ${MATRIX_SERVER_IP}:8008 {
header_down -Access-Control-Allow-Origin
header_down -Access-Control-Allow-Methods
header_down -Access-Control-Allow-Headers
header_down -Vary
}
}
handle {
reverse_proxy ${MATRIX_SERVER_IP}:8008
}
}
# =========================
# MAS (OIDC)
# =========================
${AUTH_DOMAIN} {
# OIDC Discovery
@disco path /.well-known/openid-configuration
handle @disco {
header ?Access-Control-Allow-Origin "*"
reverse_proxy ${MATRIX_SERVER_IP}:8080
}
# OAuth2 endpoints
@oauth path /oauth2/*
route @oauth {
header ?Access-Control-Allow-Origin "*"
reverse_proxy ${MATRIX_SERVER_IP}:8080
}
# Account portal
handle_path /account/* {
reverse_proxy ${MATRIX_SERVER_IP}:8080
}
handle {
reverse_proxy ${MATRIX_SERVER_IP}:8080
}
handle_errors {
header ?Access-Control-Allow-Origin "*"
}
}
# =========================
# Authelia SSO
# =========================
${AUTHELIA_DOMAIN} {
reverse_proxy ${AUTHELIA_SERVER_IP}:9091
}
# =========================
# Element Web
# =========================
${ELEMENT_DOMAIN} {
# Serve config with proper settings
@cfg path /config.json
handle @cfg {
header Content-Type application/json
header Cache-Control no-store
respond \`${PROD_ELEMENT_CFG_JSON}\` 200
}
handle {
reverse_proxy ${MATRIX_SERVER_IP}:8090
}
}
# =========================
# Element Admin
# =========================
${ADMIN_DOMAIN} {
# Proxy to Element Admin
handle {
reverse_proxy ${MATRIX_SERVER_IP}:8091
}
}
EOF
# Append identity domain well-known block if SERVER_NAME differs from MATRIX_DOMAIN
if [[ "$SERVER_NAME" != "$MATRIX_DOMAIN" ]]; then
cat >> caddy/Caddyfile.production << EOF
# =========================
# Identity Domain (well-known delegation)
# =========================
${SERVER_NAME} {
@wk path /.well-known/matrix/client
handle @wk {
header Content-Type application/json
respond \`${PROD_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
}
}
EOF
fi
# Append Element Call blocks to production Caddyfile if enabled
if [[ "$USE_ELEMENT_CALL" == true ]]; then
cat >> caddy/Caddyfile.production << EOF
# =========================
# Element Call (LiveKit)
# =========================
${RTC_DOMAIN} {
handle_path /livekit/jwt* {
reverse_proxy ${MATRIX_SERVER_IP}:8082
}
handle_path /livekit/sfu* {
reverse_proxy ${MATRIX_SERVER_IP}:7880
}
}
# =========================
# Element Call Frontend
# =========================
${CALL_DOMAIN} {
reverse_proxy ${MATRIX_SERVER_IP}:8083
}
EOF
fi
print_status "Production Caddyfile created: caddy/Caddyfile.production"
echo ""
print_info "Production configs generated successfully!"
print_status "Authelia config: authelia/config/configuration.yml"
print_status "Authelia users: authelia/config/users_database.yml"
print_status "Caddy config: caddy/Caddyfile.production"
print_status "Caddy compose: docker-compose.caddy.yml"
print_status "Authelia compose: docker-compose.authelia.yml"
echo ""
fi
# Check service status
echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN}Deployment Complete!${NC}"
echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}"
echo ""
# Show service status
echo -e "${BLUE}Service Status:${NC}"
$DOCKER_COMPOSE_CMD -f ${COMPOSE_FILE} ps
echo ""
echo -e "${GREEN}✓ Matrix stack is now running!${NC}"
echo ""
if [[ "$DEPLOYMENT_MODE" == "local" ]]; then
echo -e "${BLUE}Access Points (HTTPS with self-signed certificates):${NC}"
echo -e " • Element Web: https://${ELEMENT_DOMAIN}"
echo -e " • Matrix API: https://${MATRIX_DOMAIN}"
echo -e " • MAS (Auth): https://${AUTH_DOMAIN}"
if [[ "$USE_AUTHELIA" == true ]]; then
echo -e " • Authelia: https://${AUTHELIA_DOMAIN}"
fi
if [[ "$USE_ELEMENT_CALL" == true ]]; then
echo -e " • Element Call: https://${CALL_DOMAIN}"
echo -e " • LiveKit JWT: https://${RTC_DOMAIN}/livekit/jwt"
fi
echo -e " • Caddy Admin: http://localhost:2019"
echo ""
echo -e "${YELLOW}⚠ Self-Signed Certificate Warning:${NC}"
echo -e " Your browser will show a security warning because we're using"
echo -e " self-signed certificates for local testing. This is expected!"
echo -e " Click 'Advanced' and 'Proceed to site' to continue."
echo ""
if [[ "$USE_AUTHELIA" == true ]]; then
echo -e "${BLUE}Authelia Login Credentials:${NC}"
echo -e " • Username: admin"
echo -e " • Password: ${ADMIN_PASSWORD}"
echo -e " ${RED}⚠ SAVE THIS PASSWORD - you'll need it to log in!${NC}"
echo ""
echo -e "${BLUE}Next Steps:${NC}"
echo -e " 1. Go to https://${ELEMENT_DOMAIN}"
echo -e " 2. Accept the self-signed certificate warning"
echo -e " 3. Click 'Sign In'"
echo -e " 4. You'll be redirected through MAS → Authelia for SSO"
echo -e " 5. Log in with the Authelia credentials above"
echo -e " 6. Set up 2FA (Time-based OTP) for additional security"
echo -e " 7. Complete registration and start chatting!"
echo ""
else
echo -e "${BLUE}Next Steps:${NC}"
echo -e " 1. Go to https://${ELEMENT_DOMAIN}"
echo -e " 2. Accept the self-signed certificate warning"
echo -e " 3. Click 'Sign In'"
echo -e " 4. You'll be redirected to MAS for authentication"
echo -e " 5. Register a new account with your email and password"
echo -e " 6. Complete email verification if required"
echo -e " 7. Start chatting!"
echo ""
fi
else
# Production mode
echo -e "${BLUE}Matrix Server Deployed!${NC}"
echo ""
echo -e "${MAGENTA}Production Deployment - Next Steps:${NC}"
echo ""
echo -e "${CYAN}1. Deploy Caddy on your SSL termination machine:${NC}"
echo -e " Generated files:"
echo -e " • caddy/Caddyfile.production"
echo -e " • docker-compose.caddy.yml"
echo -e " • Copy these files to your Caddy machine"
echo -e " • Run: docker compose -f docker-compose.caddy.yml up -d"
echo ""
echo -e "${CYAN}2. Deploy Authelia on your SSO machine:${NC}"
echo -e " Generated files:"
echo -e " • authelia/config/configuration.yml"
echo -e " • authelia/config/users_database.yml"
echo -e " • docker-compose.authelia.yml"
echo -e " • Copy these files to your Authelia machine"
echo -e " • Run: docker compose -f docker-compose.authelia.yml up -d"
echo ""
echo -e "${CYAN}3. Configure DNS:${NC}"
echo -e " Point these domains to your Caddy machine (${MATRIX_SERVER_IP}):"
echo -e "${MATRIX_DOMAIN}"
echo -e "${ELEMENT_DOMAIN}"
echo -e "${AUTH_DOMAIN}"
echo -e "${AUTHELIA_DOMAIN}"
if [[ "$USE_ELEMENT_CALL" == true ]]; then
echo -e "${RTC_DOMAIN}"
echo -e "${CALL_DOMAIN}"
fi
echo ""
echo -e "${CYAN}4. Configure Firewall:${NC}"
echo -e " Matrix server (${MATRIX_SERVER_IP}): Allow from Caddy"
echo -e " Authelia server (${AUTHELIA_SERVER_IP}): Allow from Caddy and Matrix"
echo -e " Caddy: Allow ports 80/443 from internet"
if [[ "$USE_ELEMENT_CALL" == true ]]; then
echo -e " Matrix server (${MATRIX_SERVER_IP}): Allow port 7881/TCP and 50100-50200/UDP from internet (WebRTC)"
fi
echo ""
echo -e "${BLUE}Authelia Login Credentials:${NC}"
echo -e " • Username: admin"
echo -e " • Password: ${ADMIN_PASSWORD}"
echo -e " ${RED}⚠ SAVE THIS PASSWORD!${NC}"
echo ""
echo -e "${BLUE}Access URLs (after DNS and Caddy setup):${NC}"
echo -e " • Element Web: https://${ELEMENT_DOMAIN}"
echo -e " • Matrix API: https://${MATRIX_DOMAIN}"
echo -e " • MAS (Auth): https://${AUTH_DOMAIN}"
echo -e " • Authelia: https://${AUTHELIA_DOMAIN}"
if [[ "$USE_ELEMENT_CALL" == true ]]; then
echo -e " • Element Call: https://${CALL_DOMAIN}"
echo -e " • LiveKit JWT: https://${RTC_DOMAIN}/livekit/jwt"
fi
echo ""
fi
echo -e "${BLUE}Useful Commands:${NC}"
if [[ "$USE_AUTHELIA" == true ]]; then
echo -e " • View logs: $DOCKER_COMPOSE_CMD --profile authelia logs -f"
echo -e " • Stop stack: $DOCKER_COMPOSE_CMD --profile authelia down"
echo -e " • Restart service: $DOCKER_COMPOSE_CMD --profile authelia restart <service>"
echo -e " • View status: $DOCKER_COMPOSE_CMD --profile authelia ps"
else
echo -e " • View logs: $DOCKER_COMPOSE_CMD logs -f"
echo -e " • Stop stack: $DOCKER_COMPOSE_CMD down"
echo -e " • Restart service: $DOCKER_COMPOSE_CMD restart <service>"
echo -e " • View status: $DOCKER_COMPOSE_CMD ps"
fi
echo ""
echo -e "${BLUE}Generated Files:${NC}"
echo -e " • .env - Environment variables"
if [[ "$USE_AUTHELIA" == true ]]; then
echo -e " • authelia_private.pem - Authelia RSA key"
fi
echo -e " • mas-signing.key - MAS signing key"
if [[ "$USE_AUTHELIA" == true ]]; then
echo -e " • authelia/config/configuration.yml - Authelia config"
echo -e " • authelia/config/users_database.yml - User accounts"
fi
echo -e " • mas/config/config.yaml - MAS config"
echo -e " • synapse/data/homeserver.yaml - Synapse config"
echo ""
echo -e "${YELLOW}Important Notes:${NC}"
echo -e " • Using example.test domains (not .localhost) to avoid public suffix list issues"
echo -e " • All critical bugfixes have been applied (see BUGFIXES.md for details)"
echo -e " • MAS configured with assets resource and internal discovery"
if [[ "$USE_AUTHELIA" == true ]]; then
echo -e " • Authelia upstream provider enabled with fetch_userinfo and preferred_username claim"
echo -e " • SSL certificate trust configured for local development"
else
echo -e " • MAS handling password authentication directly (no upstream provider)"
fi
echo ""
echo -e "${BLUE}Troubleshooting:${NC}"
echo -e " • If CSS is missing: Check that MAS has 'assets' resource in config"
echo -e " • If login fails with empty string error: Verify fetch_userinfo: true in MAS"
echo -e " • If redirect URI error: Check Authelia client redirect_uris include upstream callback"
echo -e " • If SSL errors: Ensure mas/certs/caddy-ca.crt exists and MAS was restarted"
echo -e " • For detailed troubleshooting: See BUGFIXES.md"
echo ""
echo -e "${YELLOW}Security Note:${NC}"
echo -e " This is a local testing deployment with self-signed certificates."
echo -e " For production: Use Let's Encrypt, enable 2FA, and review all configs."
echo -e " See PRODUCTION.md for the distributed deployment guide."
echo ""