diff --git a/LICENSE b/LICENSE index 940f7d3..56e46ee 100644 --- a/LICENSE +++ b/LICENSE @@ -219,8 +219,8 @@ If you develop a new program, and you want it to be of the greatest possible use To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - ammids - Copyright (C) 2026 Main + Ammids + Copyright (C) 2026 cortex This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/README.md b/README.md index 6a20e34..d9b0afa 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,186 @@ -# ammids +# AMMIDS +**Automated Modular Matrix Infrastructure Deployment Script** +> **Status:** Beta / Experimental +> **Target OS:** Linux (Ubuntu/Debian recommended) +> **License:** AGPL-3.0 +> **Maintainer:** Cortex@Fossgate +> **Source:** [https://git.fossgate.uk/ammids](https://git.fossgate.uk/ammids) + +AMMIDS is a modular, automated deployment suite designed to spin up a production-ready, upstreamable **Matrix** infrastructure on a fresh VPS. It orchestrates **Synapse**, **Matrix Authentication Service (MAS)**, **PostgreSQL**, and optional components like **LiveKit** and various web clients, all behind a reverse proxy. + +## ⚠️ Important Prerequisites + +This script is **tested exclusively on Linux** (specifically Ubuntu 22.04+ and Debian 11+). It relies on GNU-specific utilities (e.g., `sed -i`, `stat -c`) and will **not work** on macOS, BSD, or Windows without significant modification. + +### Required Dependencies + +Before running the setup, ensure the following are installed on your VPS: + +| Dependency | Purpose | Install Command (Debian/Ubuntu) | +| :--- | :--- | :--- | +| **Docker & Docker Compose** | Container orchestration | | +| **yq** | YAML processing (Critical) | | +| **OpenSSL** | Secure password generation | (Usually pre-installed) | +| **Git** | Repository cloning | | +| **Sudo** | Permission management | | + +> **Note:** If `yq` is missing, the stack initialization will fail immediately. Ensure it is in your `$PATH` and executable. + +## 🏗️ Architecture Overview + +AMMIDS generates a modular Docker Compose stack with the following components: + +* **Core Services:** + * **Synapse:** The Matrix homeserver (supports generic workers for scaling). + * **MAS (Matrix Auth Service):** Handles OIDC authentication and user sessions. + * **PostgreSQL:** Centralized database for Synapse and MAS. + * **Redis:** Required for Synapse replication and worker communication. +* **Proxy Layer:** + * **Traefik** (Default) or **nginx**: Manages SSL termination and routing. +* **Optional Modules:** + * **LiveKit:** For voice/video call support. + * **Web Clients:** Element, Cinny, or FluffyChat. + * **Workers:** Scalable Synapse workers (Federation Senders, Media Repos, etc.). + +## 🚀 Quick Start + +### 1. Clone the Repository +``` +git clone https://git.fossgate.uk/ammids.git +cd ammids +``` +### 2. Install Dependencies +Ensure `docker`, `docker-compose`, and `yq` are installed (see **Prerequisites** above). + +### 3. Run the Orchestrator +Execute the main setup script. This will guide you through configuration and generate all necessary files. + +chmod +x setup.sh +./setup.sh + + +**What happens during setup:** +1. **Environment Generation:** Prompts for your domain, database credentials, and installation path. Creates `.env`. +2. **Directory Prep:** Creates the data volume directory and fixes ownership. +3. **Stack Initialization:** + * Starts the database and waits for readiness. + * Runs database migration scripts. + * Generates Synapse and MAS configuration files. + * Links Synapse and MAS via Native OIDC. +4. **Compose Generation:** Creates the final `compose.yaml` with your selected modules. + +### 4. Launch the Stack +Once the script completes, start the services: + +``` +docker compose up -d +``` + +Verify the status: +``` +docker compose ps +``` + +## 📂 Project Structure + +``` +/ +├── setup.sh # Main orchestrator +├── .env # Generated environment variables (DO NOT COMMIT) +├── compose.yaml # Generated Docker Compose file +├── init/ +│ ├── scripts/ +│ │ ├── set_env.sh # Interactive environment generator +│ │ ├── stack_gen.sh # Initializes DB and generates configs +│ │ ├── config_gen.sh # Alternative config generator (legacy) +│ │ ├── compose_gen.sh# Generates the compose file with toggles +│ │ ├── set_compose.sh# Updated compose file generator +│ │ ├── worker_gen.sh # Generates scalable Synapse workers +│ │ ├── set_workers.sh# Generates specific worker configs +│ │ └── link_synapse.sh# Configures OIDC between Synapse & MAS +│ ├── templates/ # YAML templates for workers and compose +│ └── config/ # Log and config templates +└── matrix/ # Data volume (created at runtime) + ├── synapse/ + ├── mas/ + └── postgres/ +``` + +## ⚙️ Configuration & Customization + +### Environment Variables (`.env`) +The `set_env.sh` script generates a secure `.env` file. Key variables include: +* `DOMAIN`: Your base domain (e.g., `example.org`). +* `HOMESERVER_FQDN`: Your Matrix server address. +* `VOLUME_PATH`: Where data is stored (defaults to `./matrix`). +* `PG_*`: Database credentials (auto-generated if left blank). +* `LIVEKIT_API_KEY`: (Optional) Enable voice/video calls. + +### Adding Workers +To scale your Synapse instance, use the worker generator: +``` +./init/scripts/worker_gen.sh -c 2 +``` +This adds 2 generic workers and updates `compose.yaml`. + +### Changing Web Clients +Re-run `./init/scripts/set_compose.sh` to toggle between Element, Cinny, FluffyChat, or disable web clients entirely. + +### OIDC Integration +The `config_link.sh` script automatically configures **Native OIDC** between Synapse and MAS. It: +1. Generates a Client ID/Secret in MAS. +2. Injects the OIDC provider configuration into Synapse. +3. Optionally configures **LiveKit** if enabled in `.env`. + +## 🛡️ Security Notes + +* **Secrets Management:** The `.env` file contains database passwords and API keys. It is set to `chmod 600` automatically. **Do not commit this file to Git.** +* **Firewall:** Ensure ports `80` and `443` are open for the proxy, and `8448` for Matrix federation. +* **Backups:** Regularly back up the `matrix/` directory and your `.env` file. Losing `.env` means losing access to your database credentials. +* **Permissions:** The scripts attempt to fix directory ownership automatically. If you encounter permission errors, ensure your user has `sudo` privileges. + +## 🐛 Troubleshooting + +### "Command not found: yq" +The script relies on `yq` for YAML manipulation. Install it manually: +``` +# Linux AMD64 +wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 +chmod +x /usr/local/bin/yq +``` + +### "compose.yaml not found" +If the script reports success but `compose.yaml` is missing: +1. Check if you are running the script from the correct directory. +2. Ensure `init/templates/compose.yaml.template` exists. +3. Check `/var/log/syslog` or `dmesg` for permission errors on the root directory. + +### Database Not Ready +If the stack fails to start with a database timeout: +``` +docker compose logs matrix-db +``` +Ensure your VPS has sufficient RAM (minimum 2GB recommended for Synapse + Postgres). + +### OIDC Linking Failed +If `link_synapse.sh` fails: +1. Verify `yq` is installed and functional. +2. Check that `mas/config.yaml` and `synapse/homeserver.yaml` were generated successfully. +3. Ensure `MAS_FQDN` is correctly set in `.env`. + +## 🤝 Contributing + +Contributions are welcome! Please open an issue on the Gitea repository before submitting large changes. + +* **Report Bugs:** [Issues Page](https://git.fossgate.uk/ammids/issues) +* **Feature Requests:** [Discussions](https://git.fossgate.uk/ammids/discussions) + +## 📜 License + +This project is licensed under the **GNU Affero General Public License v3.0 (AGPL-3.0)**. + +See the [LICENSE](LICENSE) file for details. + +--- +*AMMIDS is provided "as is", without warranty of any kind. Use at your own risk.* \ No newline at end of file diff --git a/init/config/worker-log.yaml b/init/config/worker-log.yaml new file mode 100644 index 0000000..99437cf --- /dev/null +++ b/init/config/worker-log.yaml @@ -0,0 +1,19 @@ +version: 1 +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s' +handlers: + file: + class: logging.handlers.RotatingFileHandler + filename: /var/log/synapse/{{WORKER_NAME}}.log + maxBytes: {{MAX_BYTES}} + backupCount: {{BACKUP_COUNT}} + formatter: precise +root: + level: {{LOG_LEVEL}} + handlers: [file] +loggers: + synapse: + level: {{SYNAPSE_LOG_LEVEL}} + synapse.storage: + level: WARNING diff --git a/init/config/worker.yaml b/init/config/worker.yaml new file mode 100644 index 0000000..3ca2a05 --- /dev/null +++ b/init/config/worker.yaml @@ -0,0 +1,45 @@ +# Auto-generated config for {{WORKER_NAME}} +# Generated by generate-workers.sh + +worker_app: synapse.app.generic_worker + +instance_map: + master: + host: synapse + port: 9090 + {{WORKER_NAME}}: + host: {{WORKER_NAME}} + port: {{PORT}} + +worker_name: {{WORKER_NAME}} +worker_listeners: + - port: {{PORT}} + bind_addresses: ['::'] + type: http + resources: + - names: [replication] + +worker_replication_host: synapse +worker_replication_http_port: {{PORT}} + +server_name: {{SERVER_NAME}} + +database: + name: psycopg2 + args: + user: {{DB_USER}} + password: {{DB_PASSWORD}} + database: {{DB_NAME}} + host: db + cp_min: 5 + cp_max: 10 + +redis: + enabled: true + host: redis + port: 6379 + +log_config: "/data/workers/{{WORKER_NAME}}.log.config" +worker_daemonize: true +worker_pid_file: "/var/run/synapse/{{WORKER_NAME}}.pid" +metrics_enabled: true diff --git a/init/scripts/compose_gen.sh b/init/scripts/compose_gen.sh new file mode 100755 index 0000000..403d06e --- /dev/null +++ b/init/scripts/compose_gen.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/functions.sh" +resolve_paths + +TEMPLATE_FILE="${INIT_DIR}/templates/compose.yaml.template" +OUTPUT_FILE="${ROOT_DIR}/compose.yaml" + +require_file "$TEMPLATE_FILE" "Compose template" +require_env + +log_info "Configuring optional services..." + +read -r -p "Enable LiveKit RTC Stack? (Y/n) [Y]: " LIVEKIT_INPUT +LIVEKIT_INPUT="${LIVEKIT_INPUT:-Y}" +if [[ "$LIVEKIT_INPUT" =~ ^[Yy]$ ]]; then + INCLUDE_LIVEKIT=true + LIVEKIT_KEY=$(openssl rand -hex 16) + LIVEKIT_SECRET=$(openssl rand -hex 32) + cat >> "$ENV_FILE" < /dev/null 2>&1; then + break + fi + [[ $ATTEMPT -eq 30 ]] && { log_error "Database timeout"; exit 1; } + sleep 2 +done + +# Database initialization SQL +INIT_SQL=$(cat < "${VOLUME_PATH}/mas/config.yaml" + +fix_ownership "${VOLUME_PATH}/mas" +log_info "Configuration complete" diff --git a/init/scripts/config_link.sh b/init/scripts/config_link.sh new file mode 100755 index 0000000..53d69c2 --- /dev/null +++ b/init/scripts/config_link.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/functions.sh" +resolve_paths + +require_env +require_command "yq" +require_command "openssl" + +SYNAPSE_CFG="${VOLUME_PATH}/synapse/homeserver.yaml" +MAS_CFG="${VOLUME_PATH}/mas/config.yaml" + +require_file "$SYNAPSE_CFG" "Synapse config" +require_file "$MAS_CFG" "MAS config" + +CLIENT_ID="synapse-client" +CLIENT_SECRET=$(openssl rand -hex 32) + +TEMP_FILTER=$(mktemp) +trap "rm -f $TEMP_FILTER" EXIT + +cat > "$TEMP_FILTER" < "$TEMP_FILTER" </dev/null | grep -q "idp_id"; then + yq -y -i "$(cat "$TEMP_FILTER")" "$SYNAPSE_CFG" || { log_error "Failed to inject OIDC config"; exit 1; } +fi + +if [[ -n "${LIVEKIT_API_KEY:-}" ]]; then + JWT_URL="https://${JWT_FQDN:-jwt.${DOMAIN}}" + if yq '.livekit.url' "$SYNAPSE_CFG" 2>/dev/null | grep -q "."; then + yq -y -i ".livekit.url = \"$JWT_URL\"" "$SYNAPSE_CFG" + else + yq -y -i ".livekit.url = \"$JWT_URL\"" "$SYNAPSE_CFG" + fi +fi + +log_info "OIDC and LiveKit linking complete" diff --git a/init/scripts/functions.sh b/init/scripts/functions.sh new file mode 100644 index 0000000..ad1bba7 --- /dev/null +++ b/init/scripts/functions.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# AMMIDS Shared Functions Library +# Location: init/scripts/functions.sh +# Sourced by all other scripts in this directory + +# Prevent direct execution +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + echo "This is a library, not a script. Source it instead." + exit 1 +fi + +# --- Color Codes --- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +TITLE='\033[38;2;17;97;2m' +NC='\033[0m' + +# --- Path Resolution --- +# Standardised for project structure: /init/scripts/xxx.sh +# Sets: SCRIPT_DIR, INIT_DIR, ROOT_DIR +resolve_paths() { + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[1]}")" && pwd)" + INIT_DIR="$(dirname "$SCRIPT_DIR")" + ROOT_DIR="$(dirname "$INIT_DIR")" + export SCRIPT_DIR INIT_DIR ROOT_DIR +} + +# --- Logging --- +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# --- State Management (init.ini) --- +write_state() { + local key="$1" + local value="$2" + local init_file="${INIT_DIR}/init.ini" + local upper_value + upper_value=$(echo "$value" | tr '[:lower:]' '[:upper:]') + echo "${key}=${upper_value}" >> "$init_file" +} + +load_state() { + local init_file="${INIT_DIR}/init.ini" + if [[ -f "$init_file" ]]; then + source "$init_file" + else + log_warn "State file not found at ${init_file}. Using defaults." + fi +} + +clear_state() { + local init_file="${INIT_DIR}/init.ini" + > "$init_file" +} + +# --- Secrets --- +generate_secret() { + local length="${1:-32}" + if command -v openssl &> /dev/null; then + openssl rand -hex "$((length / 2))" + elif command -v pwgen &> /dev/null; then + pwgen -s "$length" 1 + else + tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$length" + fi +} + +# --- File Permissions --- +fix_ownership() { + local target="$1" + local uid="${2:-${USER_ID:-1000}}" + local gid="${3:-${GROUP_ID:-1000}}" + + if [[ -e "$target" ]] && [[ "$(stat -c '%U' "$target" 2>/dev/null)" != "$USER" ]]; then + log_warn "Fixing ownership on $target..." + sudo chown -R "${uid}:${gid}" "$target" + fi +} + +# --- Dependency Checks --- +require_command() { + local cmd="$1" + if ! command -v "$cmd" &> /dev/null; then + log_error "Required command '${cmd}' not found. Install it and retry." + exit 1 + fi +} + +require_file() { + local file="$1" + local description="${2:-File}" + if [[ ! -f "$file" ]]; then + log_error "${description} not found at ${file}" + exit 1 + fi +} + +require_env() { + local file="${ROOT_DIR}/.env" + if [[ ! -f "$file" ]]; then + log_error ".env file not found. Run set_env.sh first." + exit 1 + fi + source "$file" +} + +require_docker() { + require_command "docker" + require_command "docker compose" +} + +# --- Volume Path Resolution --- +resolve_volume_path() { + VOLUME_PATH="${VOLUME_PATH:-./matrix}" + if [[ "$VOLUME_PATH" != /* ]]; then + VOLUME_PATH="${ROOT_DIR}/${VOLUME_PATH}" + fi + export VOLUME_PATH +} + +# Add to functions.sh + + +quiet_echo() { + [[ "${QUIET_MODE:-0}" == "1" ]] || echo "$1" +} diff --git a/init/scripts/set_env.sh b/init/scripts/set_env.sh new file mode 100755 index 0000000..bebcdfd --- /dev/null +++ b/init/scripts/set_env.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/functions.sh" +resolve_paths + +require_command "openssl" || require_command "pwgen" + +main() { + > "$ENV_FILE" + + read -r -p "Base Domain (e.g. example.org) [example.org]: " DOMAIN_INPUT + BASE_DOMAIN="${DOMAIN_INPUT:-example.org}" + + cat >> "$ENV_FILE" < "$TEMP_COMPOSE" + +for ((i=1; i<=WORKER_COUNT; i++)); do + worker_name="synapse-worker-${i}" + port=$((9093 + i)) + + sed -e "s/{{WORKER_NAME}}/${worker_name}/g" \ + -e "s/{{PORT}}/${port}/g" \ + -e "s/{{SERVER_NAME}}/${HOMESERVER_FQDN}/g" \ + -e "s/{{DB_USER}}/${DB_USER:-synapse}/g" \ + -e "s/{{DB_PASSWORD}}/${DB_PASSWORD:-synapse_password}/g" \ + -e "s/{{DB_NAME}}/${DB_NAME:-synapse}/g" \ + "$TEMPLATE_CONFIG" > "${WORKERS_DIR}/${worker_name}.yaml" + + sed -e "s/{{WORKER_NAME}}/${worker_name}/g" \ + -e "s/{{MAX_BYTES}}/${LOG_MAX_BYTES:-10485760}/g" \ + -e "s/{{BACKUP_COUNT}}/${LOG_BACKUP_COUNT:-5}/g" \ + -e "s/{{LOG_LEVEL}}/${LOG_LEVEL:-INFO}/g" \ + -e "s/{{SYNAPSE_LOG_LEVEL}}/${SYNAPSE_LOG_LEVEL:-INFO}/g" \ + "$TEMPLATE_LOG" > "${WORKERS_DIR}/${worker_name}.log.config" + + TRAEFIK_LABELS="" + if [[ "$TRAEFIK_ENABLED" == "1" && -n "$DOMAIN" ]]; then + TRAEFIK_LABELS=" labels: + - traefik.enable=true + - traefik.http.services.${worker_name}.loadbalancer.server.port=${port} + - traefik.http.routers.synapse-worker-${worker_name}.rule=Host(\`${DOMAIN}\`) && PathPrefix(\`/_matrix\`) + - traefik.http.routers.synapse-worker-${worker_name}.entrypoints=websecure + - traefik.http.routers.synapse-worker-${worker_name}.service=${worker_name} + - traefik.http.routers.synapse-worker-${worker_name}.priority=10 +" + fi + + sed -e "s/{{WORKER_NAME}}/${worker_name}/g" \ + -e "s/{{PORT}}/${port}/g" \ + -e "s/{{APP_TYPE}}/synapse.app.generic_worker/g" \ + -e "s/{{TRAEFIK_LABELS}}/${TRAEFIK_LABELS}/g" \ + "$TEMPLATE_COMPOSE" >> "$TEMP_COMPOSE" + + echo "" >> "$TEMP_COMPOSE" +done + +if [[ -f "$COMPOSE_FILE" ]]; then + if ! grep -qE "^ synapse-worker-[0-9]+:" "$COMPOSE_FILE"; then + cat "$TEMP_COMPOSE" >> "$COMPOSE_FILE" + log_info "Appended ${WORKER_COUNT} worker(s) to compose.yaml" + else + log_warn "Some workers already exist. Manual merge required" + cp "$TEMP_COMPOSE" "${ROOT_DIR}/.tmp/compose_workers.yaml" + fi +else + log_warn "compose.yaml not found. Generated: ${TEMP_COMPOSE}" +fi + +rm -f "$TEMP_COMPOSE" +log_info "Worker generation complete" diff --git a/init/templates/element-config.json b/init/templates/element-config.json new file mode 100644 index 0000000..48f03fb --- /dev/null +++ b/init/templates/element-config.json @@ -0,0 +1,10 @@ +{ + "default_server_config": { + "m.homeserver": { + "base_url": "https://${HOMESERVER_FQDN}", + "server_name": "${HOMESERVER_FQDN}" + } + }, + "default_server_name": "${HOMESERVER_FQDN}", + "brand": "Element" +} diff --git a/init/templates/main.yaml b/init/templates/main.yaml new file mode 100644 index 0000000..114c27a --- /dev/null +++ b/init/templates/main.yaml @@ -0,0 +1,241 @@ +# COMPOSE TEMPLATE - DO NOT EDIT MANUALLY +# Generated by set_compose.sh + +networks: + matrix_network: + name: matrix_network + +services: + # --- CORE SERVICES (Always Included) --- + matrix-db: + image: postgres:latest + container_name: "matrix-db" + restart: unless-stopped + volumes: + - ${VOLUME_PATH}/postgres:/var/lib/postgresql:rw + networks: + - matrix_network + environment: + POSTGRES_USER: ${PG_USER} + POSTGRES_PASSWORD: ${PG_PASSWORD} + POSTGRES_INITDB_ARGS: --encoding=UTF8 --locale=C + healthcheck: + test: ["CMD-SHELL", "pg_isready -U '${PG_USER}'"] + start_period: "1s" + interval: "1s" + timeout: "5s" + + redis: + image: redis:latest + container_name: "matrix-redis" + restart: unless-stopped + networks: + - matrix_network + command: redis-server --appendonly yes + + synapse: + image: ghcr.io/element-hq/synapse:latest + container_name: "synapse" + user: ${USER_ID}:${GROUP_ID} + restart: unless-stopped + volumes: + - ${VOLUME_PATH}/synapse:/data:rw + networks: + - matrix_network + environment: + SYNAPSE_CONFIG_DIR: /data + SYNAPSE_CONFIG_PATH: /data/homeserver.yaml + depends_on: + redis: + condition: service_started + matrix-db: + condition: service_healthy + # [TRAEFIK_LABELS_START] + labels: + - "traefik.enable=true" + - "traefik.http.routers.synapse.rule=Host(`${HOMESERVER_FQDN}`)" + - "traefik.http.routers.synapse.entrypoints=websecure" + - "traefik.http.routers.synapse.tls.certresolver=myresolver" + - "traefik.http.services.synapse.loadbalancer.server.port=8008" + # [TRAEFIK_LABELS_END] + + mas: + image: ghcr.io/element-hq/matrix-authentication-service:latest + container_name: "mas" + restart: unless-stopped + volumes: + - ${VOLUME_PATH}/mas:/data:rw + networks: + - matrix_network + command: "server --config=/data/config.yaml" + depends_on: + matrix-db: + condition: service_healthy + # [TRAEFIK_LABELS_START] + labels: + - "traefik.enable=true" + - "traefik.http.routers.auth.rule=Host(`${MAS_FQDN}`)" + - "traefik.http.routers.auth.entrypoints=websecure" + - "traefik.http.routers.auth.tls.certresolver=myresolver" + - "traefik.http.services.auth.loadbalancer.server.port=8000" + # [TRAEFIK_LABELS_END] + + # --- OPTIONAL: LIVEKIT RTC STACK --- + # [LIVEKIT_START] + livekit: + image: livekit/livekit-server:latest + container_name: "livekit" + restart: unless-stopped + volumes: + - ${VOLUME_PATH}/livekit:/etc/livekit + environment: + - LIVEKIT_KEYS=access-key=${LIVEKIT_API_KEY},secret=${LIVEKIT_API_SECRET} + - LIVEKIT_PORT=7880 + - LIVEKIT_WS_PORT=7881 + - LIVEKIT_SERVER_URL=https://${LIVEKIT_FQDN} + networks: + - matrix_network + depends_on: + - redis + - matrix-db + ports: + - "${LIVEKIT_UDP_RANGE}:${LIVEKIT_UDP_RANGE}/udp" + # [TRAEFIK_LABELS_START] + labels: + - "traefik.enable=true" + - "traefik.http.routers.rtc.rule=Host(`${LIVEKIT_FQDN}`)" + - "traefik.http.routers.rtc.entrypoints=websecure" + - "traefik.http.routers.rtc.tls.certresolver=myresolver" + - "traefik.http.services.rtc.loadbalancer.server.port=7880" + # [TRAEFIK_LABELS_END] + + # The JWT Bridge Service + lk-jwt-service: + image: ghcr.io/element-hq/lk-jwt-service:latest + container_name: "lk-jwt-service" + restart: unless-stopped + environment: + - LK_JWT_PORT=8080 + - LIVEKIT_URL=wss://${LIVEKIT_FQDN} + - LIVEKIT_KEY=${LIVEKIT_API_KEY} + - LIVEKIT_SECRET=${LIVEKIT_API_SECRET} + - LIVEKIT_LOCAL_HOMESERVERS=${HOMESERVER_FQDN} + networks: + - matrix_network + depends_on: + - livekit + # [TRAEFIK_LABELS_START] + labels: + - "traefik.enable=true" + - "traefik.http.routers.jwt.rule=Host(`${JWT_FQDN}`)" + - "traefik.http.routers.jwt.entrypoints=websecure" + - "traefik.http.routers.jwt.tls.certresolver=myresolver" + - "traefik.http.services.jwt.loadbalancer.server.port=8080" + # [TRAEFIK_LABELS_END] + # [LIVEKIT_END] + + # --- OPTIONAL: WEB HOSTED APP --- + # [ELEMENT_START] + element-web: + image: vectorim/element-web:latest + container_name: "matrix-web-client" + restart: unless-stopped + volumes: + - ${VOLUME_PATH}/element-web:/usr/share/nginx/html:ro + networks: + - matrix_network + environment: + - ELEMENT_WEB_CONFIG_PATH=/usr/share/nginx/html/config.json + # [TRAEFIK_LABELS_START] + labels: + - "traefik.enable=true" + - "traefik.http.routers.app.rule=Host(`${APP_FQDN}`)" + - "traefik.http.routers.app.entrypoints=websecure" + - "traefik.http.routers.app.tls.certresolver=myresolver" + - "traefik.http.services.app.loadbalancer.server.port=80" + # [TRAEFIK_LABELS_END] + # [ELEMENT_END] + + # [CINNY_START] + cinny: + image: cinny-web:latest + container_name: "matrix-web-client" + restart: unless-stopped + volumes: + - ${VOLUME_PATH}/cinny:/usr/share/nginx/html:ro + networks: + - matrix_network + # [TRAEFIK_LABELS_START] + labels: + - "traefik.enable=true" + - "traefik.http.routers.app.rule=Host(`${APP_FQDN}`)" + - "traefik.http.routers.app.entrypoints=websecure" + - "traefik.http.routers.app.tls.certresolver=myresolver" + - "traefik.http.services.app.loadbalancer.server.port=80" + # [TRAEFIK_LABELS_END] + # [CINNY_END] + + # [FLUFFYCHAT_START] + fluffy-chat: + image: kibaek/fluffy-chat:latest + container_name: "matrix-web-client" + restart: unless-stopped + volumes: + - ${VOLUME_PATH}/fluffy-chat:/usr/share/nginx/html:ro + networks: + - matrix_network + # [TRAEFIK_LABELS_START] + labels: + - "traefik.enable=true" + - "traefik.http.routers.app.rule=Host(`${APP_FQDN}`)" + - "traefik.http.routers.app.entrypoints=websecure" + - "traefik.http.routers.app.tls.certresolver=myresolver" + - "traefik.http.services.app.loadbalancer.server.port=80" + # [TRAEFIK_LABELS_END] + # [FLUFFYCHAT_END] + + # --- OPTIONAL: REVERSE PROXY --- + # [NGINX_START] + nginx: + image: nginx:latest + container_name: "matrix-nginx" + restart: unless-stopped + ports: + - "80:80" + - "443:443" + stop_grace_period: 0s + volumes: + - ${VOLUME_PATH}/nginx/conf.d:/etc/nginx/conf.d:ro + command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" + networks: + matrix_network: + aliases: + - ${DOMAIN} + - ${HOMESERVER_FQDN} + - ${MAS_FQDN} + - ${APP_FQDN} + # [NGINX_END] + + # [TRAEFIK_START] + traefik: + image: traefik:latest + container_name: "traefik" + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ${VOLUME_PATH}/traefik:/etc/traefik + command: + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--certificatesresolvers.myresolver.acme.tlschallenge=true" + - "--certificatesresolvers.myresolver.acme.email=${PG_USER}@${DOMAIN}" + - "--certificatesresolvers.myresolver.acme.storage=/etc/traefik/acme.json" + networks: + - matrix_network + # [TRAEFIK_END] diff --git a/init/templates/worker.yaml b/init/templates/worker.yaml new file mode 100644 index 0000000..bc58cae --- /dev/null +++ b/init/templates/worker.yaml @@ -0,0 +1,20 @@ + {{WORKER_NAME}}: + image: ghcr.io/element-hq/synapse:latest + container_name: "{{WORKER_NAME}}" + user: ${USER_ID}:${GROUP_ID} + restart: unless-stopped + entrypoint: ["/start.py", "run", "--config-path=/data/homeserver.yaml", "--config-path=/data/workers/{{WORKER_NAME}}.yaml"] + networks: + - matrix_network + volumes: + - ${VOLUME_PATH}/synapse:/data:rw + - ${VOLUME_PATH}/synapse/logs:/var/log/synapse:rw + environment: + SYNAPSE_WORKER: {{APP_TYPE}} + depends_on: + - synapse + - db + - redis + ports: + - "{{PORT}}:{{PORT}}" + {{TRAEFIK_LABELS}} diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..c2786c9 --- /dev/null +++ b/setup.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# AMMIDS Setup Orchestrator +# Author: Cortex@Fossgate +# Src: https://git.fossgate.uk/ammids + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/init/scripts/functions.sh" +resolve_paths + +ENV_FILE="${ROOT_DIR}/.env" +BACKUP_DIR="${ROOT_DIR}/backups" + +log_info "=== AMMIDS Setup Orchestrator ===" + +# 1. Environment Setup +if [[ -f "$ENV_FILE" ]]; then + log_warn ".env exists." + read -r -p " Overwrite and create backup? [y/N]: " confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + mkdir -p "$BACKUP_DIR" + BACKUP_FILE="${BACKUP_DIR}/.env.$(date +%Y%m%d_%H%M%S)" + cp "$ENV_FILE" "$BACKUP_FILE" + log_info "Backup created: $BACKUP_FILE" + "${SCRIPT_DIR}/init/scripts/set_env.sh" + else + log_info "Preserving existing .env. Skipping regeneration." + fi +else + log_info "No .env found. Running initial setup..." + "${SCRIPT_DIR}/init/scripts/set_env.sh" +fi + +# 2. Load Environment & Resolve Paths +source "$ENV_FILE" +resolve_volume_path + +# 3. Directory & Permission Management +if [[ ! -d "$VOLUME_PATH" ]]; then + log_info "Creating data directory: $VOLUME_PATH" + mkdir -p "$VOLUME_PATH" +fi + +fix_ownership "$VOLUME_PATH" + +# 4. Stack Generation +log_info "Generating compose and configurations..." +"${SCRIPT_DIR}/init/scripts/compose_gen.sh" +"${SCRIPT_DIR}/init/scripts/config_gen.sh" +"${SCRIPT_DIR}/init/scripts/config_link.sh" + +# 5. Worker Generation +read -r -p "Number of worker processes? [Default: 1]: " worker_count +worker_count="${worker_count:-1}" + +if ! [[ "$worker_count" =~ ^[0-9]+$ ]]; then + log_error "Invalid number. Aborting worker generation." + exit 1 +fi + +if [[ "$worker_count" -gt 0 ]]; then + log_info "Generating $worker_count worker(s)..." + "${SCRIPT_DIR}/init/scripts/worker_gen.sh" -c "$worker_count" +else + log_info "Skipping worker generation (count: 0)." +fi + +log_info "Setup complete. Run 'docker compose up -d' to start services."