Initial Commit

This commit is contained in:
cortex
2026-05-20 10:28:49 +01:00
parent a5b3ddd701
commit 784cca8b27
14 changed files with 1035 additions and 3 deletions
+2 -2
View File
@@ -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.
+185 -1
View File
@@ -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 | <!-- curl -fsSL https://get.docker.com | sh --> |
| **yq** | YAML processing (Critical) | <!-- wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && chmod +x /usr/local/bin/yq --> |
| **OpenSSL** | Secure password generation | <!-- apt install openssl --> (Usually pre-installed) |
| **Git** | Repository cloning | <!-- apt install git --> |
| **Sudo** | Permission management | <!-- apt install sudo --> |
> **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.*
+19
View File
@@ -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
+45
View File
@@ -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
+67
View File
@@ -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" <<EOF
LIVEKIT_FQDN="rtc.${DOMAIN}"
JWT_FQDN="jwt.${DOMAIN}"
LIVEKIT_API_KEY="${LIVEKIT_KEY}"
LIVEKIT_API_SECRET="${LIVEKIT_SECRET}"
LIVEKIT_UDP_RANGE="10000-20000"
EOF
else
INCLUDE_LIVEKIT=false
fi
echo "Select Web Client: 1) Element 2) Cinny 3) FluffyChat 4) None"
read -r -p "Default [1]: " WEB_APP_INPUT
WEB_APP_INPUT="${WEB_APP_INPUT:-1}"
case "$WEB_APP_INPUT" in
1) INCLUDE_ELEMENT=true; INCLUDE_CINNY=false; INCLUDE_FLUFFY=false ;;
2) INCLUDE_ELEMENT=false; INCLUDE_CINNY=true; INCLUDE_FLUFFY=false ;;
3) INCLUDE_ELEMENT=false; INCLUDE_CINNY=false; INCLUDE_FLUFFY=true ;;
*) INCLUDE_ELEMENT=false; INCLUDE_CINNY=false; INCLUDE_FLUFFY=false ;;
esac
echo "Select Reverse Proxy: 1) Traefik 2) nginx"
read -r -p "Default [1]: " PROXY_INPUT
PROXY_INPUT="${PROXY_INPUT:-1}"
case "$PROXY_INPUT" in
1) INCLUDE_NGINX=false; INCLUDE_TRAEFIK=true ;;
*) INCLUDE_NGINX=true; INCLUDE_TRAEFIK=false ;;
esac
cp "$TEMPLATE_FILE" "$OUTPUT_FILE"
toggle_block() {
[[ "$3" == "true" ]] && sed -i "/$1/d; /$2/d" "$OUTPUT_FILE" || sed -i "/$1/,/$2/d" "$OUTPUT_FILE"
}
toggle_block "# \[LIVEKIT_START\]" "# \[LIVEKIT_END\]" "$INCLUDE_LIVEKIT"
toggle_block "# \[ELEMENT_START\]" "# \[ELEMENT_END\]" "$INCLUDE_ELEMENT"
toggle_block "# \[CINNY_START\]" "# \[CINNY_END\]" "$INCLUDE_CINNY"
toggle_block "# \[FLUFFYCHAT_START\]" "# \[FLUFFYCHAT_END\]" "$INCLUDE_FLUFFY"
toggle_block "# \[NGINX_START\]" "# \[NGINX_END\]" "$INCLUDE_NGINX"
toggle_block "# \[TRAEFIK_START\]" "# \[TRAEFIK_END\]" "$INCLUDE_TRAEFIK"
[[ "$INCLUDE_TRAEFIK" == "true" ]] && sed -i "/# \[TRAEFIK_LABELS_START\]/d; /# \[TRAEFIK_LABELS_END\]/d" "$OUTPUT_FILE" || \
sed -i "/# \[TRAEFIK_LABELS_START\]/,/# \[TRAEFIK_LABELS_END\]/d" "$OUTPUT_FILE"
log_info "Compose file generated: $OUTPUT_FILE"
+63
View File
@@ -0,0 +1,63 @@
#!/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_docker
resolve_volume_path
mkdir -p "${VOLUME_PATH}/synapse" "${VOLUME_PATH}/mas" "${VOLUME_PATH}/postgres"
log_info "Initializing PostgreSQL..."
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d --wait --timeout 60 matrix-db
# Wait for database readiness (silent except on failure)
for ATTEMPT in $(seq 1 30); do
if docker compose -f "$COMPOSE_FILE" exec -T matrix-db psql -U "$PG_USER" -c "SELECT 1;" > /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 <<EOF
DO \$\$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'mas') THEN
CREATE ROLE mas WITH LOGIN PASSWORD '${PG_PASSWORD_MAS}';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'matrix') THEN
CREATE ROLE matrix WITH LOGIN PASSWORD '${PG_PASSWORD_MATRIX}';
END IF;
END \$\$;
SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'auth')
THEN format('CREATE DATABASE %I OWNER %I', 'auth', 'mas') ELSE NULL END AS cmd \gexec
SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'synapse')
THEN format('CREATE DATABASE %I OWNER %I', 'synapse', 'matrix') ELSE NULL END AS cmd \gexec
EOF
)
echo "$INIT_SQL" | docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T matrix-db psql -U "$PG_USER" -f -
log_info "Generating Synapse configuration..."
docker run -it --rm --network matrix_network \
-v "${VOLUME_PATH}/synapse:/data" \
-e SYNAPSE_SERVER_NAME="$HOMESERVER_FQDN" \
-e SYNAPSE_REPORT_STATS=no \
ghcr.io/element-hq/synapse:latest generate
fix_ownership "${VOLUME_PATH}/synapse"
log_info "Generating MAS configuration..."
docker run -it --rm --network matrix_network \
-v "${VOLUME_PATH}/mas:/data" \
ghcr.io/element-hq/matrix-authentication-service:latest config generate \
| grep -v '^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}' > "${VOLUME_PATH}/mas/config.yaml"
fix_ownership "${VOLUME_PATH}/mas"
log_info "Configuration complete"
+54
View File
@@ -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" <<EOF
.oidc.providers += [{"id": "synapse", "client_id": "${CLIENT_ID}", "client_secret": "${CLIENT_SECRET}", "scopes": ["openid", "profile", "email"], "issuer": "http://mas:8000"}]
EOF
yq -y -i "$(cat "$TEMP_FILTER")" "$MAS_CFG" || { log_error "Failed to update MAS config"; exit 1; }
CLIENT_ID=$(yq '.oidc.providers[0].client_id' "$MAS_CFG" | tr -d '"')
CLIENT_SECRET=$(yq '.oidc.providers[0].client_secret' "$MAS_CFG" | tr -d '"')
ISSUER_URL="https://${MAS_FQDN}"
fix_ownership "$SYNAPSE_CFG"
cat > "$TEMP_FILTER" <<EOF
.oidc_providers += [{"idp_id": "mas", "idp_name": "Matrix Authentication Service", "idp_brand": "mas", "issuer": "${ISSUER_URL}", "client_id": "${CLIENT_ID}", "client_secret": "${CLIENT_SECRET}", "scopes": ["openid", "profile"], "skip_verification": false}]
EOF
if ! yq '.oidc_providers' "$SYNAPSE_CFG" 2>/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"
+129
View File
@@ -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"
}
+44
View File
@@ -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" <<EOF
DOMAIN="${BASE_DOMAIN}"
HOMESERVER_FQDN="matrix.${BASE_DOMAIN}"
MAS_FQDN="auth.${BASE_DOMAIN}"
APP_FQDN="app.${BASE_DOMAIN}"
EOF
log_info "Base Domain set to: ${BASE_DOMAIN}"
prompt_var "USER_ID" "Docker UserId" "1000" "no"
prompt_var "GROUP_ID" "Docker GroupID" "1000" "no"
prompt_var "VOLUME_PATH" "Install directory" "./matrix" "no"
prompt_var "COUNTRY" "Country Code (e.g. GB)" "GB" "no"
prompt_var "PG_USER" "PostgreSQL Superuser" "postgres" "no"
prompt_var "PG_PASSWORD" "PostgreSQL Superuser Password" "generated" "yes"
prompt_var "PG_USER_MATRIX" "Synapse Database User" "matrix" "no"
prompt_var "PG_DB_MATRIX" "Synapse Database Name" "synapse" "no"
prompt_var "PG_PASSWORD_MATRIX" "Synapse Database Password" "generated" "yes"
prompt_var "PG_USER_MAS" "MAS Database User" "mas" "no"
prompt_var "PG_DB_MAS" "MAS Database Name" "auth" "no"
prompt_var "PG_PASSWORD_MAS" "MAS Database Password" "generated" "yes"
chmod 600 "$ENV_FILE"
log_info "Environment file created: $ENV_FILE"
log_warn "Add $ENV_FILE to .gitignore and backup securely"
}
main "$@"
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/functions.sh"
resolve_paths
WORKER_COUNT=1
while getopts "c:" opt; do
case $opt in
c) WORKER_COUNT="$OPTARG" ;;
*) echo "Usage: $0 [-c count]"; exit 1 ;;
esac
done
require_env
resolve_volume_path
TEMPLATE_COMPOSE="${INIT_DIR}/templates/worker.yaml"
TEMPLATE_CONFIG="${INIT_DIR}/config/worker.yaml"
TEMPLATE_LOG="${INIT_DIR}/config/worker-log.yaml"
require_file "$TEMPLATE_COMPOSE" "Compose template"
require_file "$TEMPLATE_CONFIG" "Config template"
require_file "$TEMPLATE_LOG" "Log template"
WORKERS_DIR="${VOLUME_PATH}/synapse/workers"
mkdir -p "$WORKERS_DIR" "${ROOT_DIR}/.tmp"
TRAEFIK_ENABLED="${PROXY:-0}"
TEMP_COMPOSE="${ROOT_DIR}/.tmp/compose_workers.yaml"
> "$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"
+10
View File
@@ -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"
}
+241
View File
@@ -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]
+20
View File
@@ -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}}
Executable
+69
View File
@@ -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."