23 changed files with 793 additions and 0 deletions
@ -0,0 +1,12 @@ |
|||||
|
##
|
||||
|
## Makefile for Matrix quadlet
|
||||
|
##
|
||||
|
|
||||
|
DEPENDENCIES = traefik |
||||
|
|
||||
|
# Matrix quadlet is mapped to the 10031 user (matrix) and 10000 group (itix-svc)
|
||||
|
PROJECT_UID = 10031 |
||||
|
PROJECT_GID = 10000 |
||||
|
|
||||
|
# Include common Makefile
|
||||
|
include ../../scripts/common.mk |
||||
@ -0,0 +1,168 @@ |
|||||
|
# Podman Quadlet: Matrix |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
[Matrix](https://matrix.org/) is an open, decentralised protocol for secure and interoperable communication. This cookbook runs a self-hosted Matrix homeserver using **[Tuwunel](https://matrix-construct.github.io/tuwunel/)** (a high-performance Rust-based homeserver) together with the **[Element Web](https://element.io/)** client. |
||||
|
|
||||
|
This cookbook runs the following services: |
||||
|
|
||||
|
- **matrix-homeserver**: The Tuwunel Matrix homeserver. |
||||
|
- **matrix-web**: The Element Web browser client. |
||||
|
- **matrix-init**: One-shot first-run initialisation (creates the admin account and discovers the admin room). |
||||
|
- **matrix-backup**: Daily backup job triggered by a systemd timer. |
||||
|
- **matrix-restore**: One-shot restore job that seeds the database from the latest backup archive on first boot (if needed). |
||||
|
- **matrix-tools**: Custom OCI image (built locally from CentOS Stream 10) used by the init, backup, and restore jobs. |
||||
|
|
||||
|
The homeserver uses a RocksDB database stored locally and keeps media files and backup archives on a virtiofs volume. |
||||
|
|
||||
|
## Prerequisites |
||||
|
|
||||
|
- The `traefik` cookbook must be installed and running. |
||||
|
- A virtiofs data volume must be mounted at `/var/lib/virtiofs/data/`. |
||||
|
- Configuration files must exist before starting (see [Configuration](#configuration)). |
||||
|
|
||||
|
## Configuration |
||||
|
|
||||
|
### Homeserver — `tuwunel.env` |
||||
|
|
||||
|
Copy the example and adapt it to your environment: |
||||
|
|
||||
|
```sh |
||||
|
sudo cp config/examples/tuwunel.env /etc/quadlets/matrix/tuwunel.env |
||||
|
sudo chmod 600 /etc/quadlets/matrix/tuwunel.env |
||||
|
``` |
||||
|
|
||||
|
> **Important**: `TUWUNEL_SERVER_NAME` determines the Matrix identity of every user |
||||
|
> (e.g. `@user:example.com`). **It cannot be changed after the first start.** |
||||
|
> Prefer a root domain so users get clean handles, and delegate the actual HTTP |
||||
|
> traffic to a subdomain (e.g. `matrix.example.com`) via `.well-known`. |
||||
|
|
||||
|
Key settings: |
||||
|
|
||||
|
| Variable | Description | |
||||
|
|---|---| |
||||
|
| `TUWUNEL_SERVER_NAME` | Matrix server name — sets user IDs (`@user:<server_name>`). | |
||||
|
| `TUWUNEL_PORT` | Port Tuwunel listens on (default: `6167`). | |
||||
|
| `TUWUNEL_ALLOW_REGISTRATION` | Enable/disable new account registration. | |
||||
|
| `TUWUNEL_REGISTRATION_TOKEN` | Token required for registration (set a strong secret). | |
||||
|
| `TUWUNEL_GRANT_ADMIN_TO_FIRST_USER` | Automatically grants admin rights to the first registered user. | |
||||
|
| `TUWUNEL_ALLOW_FEDERATION` | Enable Matrix federation (disabled by default for private servers). | |
||||
|
| `MATRIX_INIT_ADMIN_USER` | Username for the initial admin account. | |
||||
|
| `MATRIX_INIT_ADMIN_PASSWORD` | Password for the initial admin account. | |
||||
|
| `MATRIX_BACKUP_RETENTION` | Number of daily backup archives to keep (default: `7`). | |
||||
|
|
||||
|
### Element Web — `element-web/config.json` |
||||
|
|
||||
|
```sh |
||||
|
sudo mkdir -p /etc/quadlets/matrix/element-web |
||||
|
sudo cp config/examples/element-web/config.json /etc/quadlets/matrix/element-web/config.json |
||||
|
``` |
||||
|
|
||||
|
Update `base_url` and `server_name` to match your deployment: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"default_server_config": { |
||||
|
"m.homeserver": { |
||||
|
"base_url": "https://matrix.example.com", |
||||
|
"server_name": "example.com" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Traefik integration |
||||
|
|
||||
|
Copy the example Traefik dynamic configuration: |
||||
|
|
||||
|
```sh |
||||
|
sudo cp other/traefik/matrix.yaml /etc/traefik/dynamic/matrix.yaml |
||||
|
``` |
||||
|
|
||||
|
This configures two routes: |
||||
|
|
||||
|
| Hostname | Backend | Service | |
||||
|
|---|---|---| |
||||
|
| `matrix.example.com` | `http://127.0.0.1:6167` | Tuwunel homeserver | |
||||
|
| `matrix-chat.example.com` | `http://127.0.0.1:8080` | Element Web client | |
||||
|
|
||||
|
Adjust the `Host()` rules and entry points to match your Traefik setup. |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
In a separate terminal, follow the logs: |
||||
|
|
||||
|
```sh |
||||
|
sudo make tail-logs |
||||
|
``` |
||||
|
|
||||
|
Install the Podman Quadlets and start Matrix: |
||||
|
|
||||
|
```sh |
||||
|
sudo make clean install |
||||
|
``` |
||||
|
|
||||
|
Services start in this order: |
||||
|
|
||||
|
1. **matrix-tools-build.service** builds the local `matrix-tools` image. |
||||
|
2. **matrix-restore.service** restores the database from the latest backup (only on first boot when no database exists yet and at least one backup archive is available). |
||||
|
3. **matrix-homeserver.service** starts Tuwunel. |
||||
|
4. **matrix-web.service** starts Element Web. |
||||
|
5. **matrix-init.service** runs once on first boot: registers the admin user, discovers the admin room, and writes backup credentials to `/var/lib/virtiofs/data/matrix/tuwunel-backup.env`. |
||||
|
6. **matrix-backup.timer** schedules the daily backup job. |
||||
|
|
||||
|
Access the Element Web client through Traefik (e.g. `https://matrix-chat.example.com`). |
||||
|
|
||||
|
Restart the **matrix.target** unit: |
||||
|
|
||||
|
```sh |
||||
|
sudo systemctl restart matrix.target |
||||
|
``` |
||||
|
|
||||
|
Finally, remove the quadlets, their configuration and their data: |
||||
|
|
||||
|
```sh |
||||
|
sudo make uninstall clean |
||||
|
``` |
||||
|
|
||||
|
## Backup and restore |
||||
|
|
||||
|
### Backup |
||||
|
|
||||
|
Backups run daily via the `matrix-backup.timer`. The backup job: |
||||
|
|
||||
|
1. Sends the `!admin server backup-database` command to the Tuwunel admin room via the Matrix API (using credentials generated by `matrix-init`). |
||||
|
2. Waits for Tuwunel to write an incremental RocksDB backup to the staging area. |
||||
|
3. Packages the backup into a restore-ready `.tar.gz` archive under `/var/lib/virtiofs/data/matrix/tuwunel/backup/`. |
||||
|
4. Applies the retention policy, keeping only the last `MATRIX_BACKUP_RETENTION` archives. |
||||
|
|
||||
|
To trigger a manual backup: |
||||
|
|
||||
|
```sh |
||||
|
sudo systemctl start matrix-backup.service |
||||
|
``` |
||||
|
|
||||
|
### Restore |
||||
|
|
||||
|
The `matrix-restore.service` runs automatically before the homeserver starts if: |
||||
|
|
||||
|
- No RocksDB database exists at `/var/lib/quadlets/matrix/tuwunel/db/`, **and** |
||||
|
- At least one `.tar.gz` backup archive exists under `/var/lib/virtiofs/data/matrix/tuwunel/backup/`. |
||||
|
|
||||
|
It extracts the latest archive into the database directory, then lets the homeserver start normally. |
||||
|
|
||||
|
## Storage layout |
||||
|
|
||||
|
| Path | Contents | |
||||
|
|---|---| |
||||
|
| `/var/lib/quadlets/matrix/tuwunel/db/` | RocksDB database (local disk). | |
||||
|
| `/var/lib/quadlets/matrix/tuwunel/backup-staging/` | RocksDB online backup staging area (cleared after each backup). | |
||||
|
| `/var/lib/virtiofs/data/matrix/tuwunel/media/` | Uploaded media files (virtiofs). | |
||||
|
| `/var/lib/virtiofs/data/matrix/tuwunel/backup/` | Daily backup archives (virtiofs). | |
||||
|
| `/var/lib/virtiofs/data/matrix/tuwunel-backup.env` | Auto-generated backup credentials (written by `matrix-init`). | |
||||
|
|
||||
|
## Integration tests |
||||
|
|
||||
|
```sh |
||||
|
sudo make test |
||||
|
``` |
||||
@ -0,0 +1,86 @@ |
|||||
|
#!/bin/bash |
||||
|
set -Eeuo pipefail |
||||
|
|
||||
|
MATRIX_URL="http://127.0.0.1:${TUWUNEL_PORT:-6167}" |
||||
|
BACKUP_RETENTION="${MATRIX_BACKUP_RETENTION:-7}" |
||||
|
ADMIN_ACCESS_TOKEN="${MATRIX_BACKUP_ACCESS_TOKEN}" |
||||
|
ADMIN_ROOM_ID="${MATRIX_BACKUP_ROOM_ID}" |
||||
|
BACKUP_TIMEOUT="${MATRIX_BACKUP_TIMEOUT:-300}" |
||||
|
BACKUP_CHECK_INTERVAL="${MATRIX_BACKUP_CHECK_INTERVAL:-30}" |
||||
|
|
||||
|
MARKER="$(mktemp)" |
||||
|
STAGING="$(mktemp -d)" |
||||
|
trap 'rm -rf "$MARKER" "$STAGING"' EXIT |
||||
|
|
||||
|
echo "Triggering Tuwunel online database backup..." |
||||
|
|
||||
|
# Send '!admin server backup-database' to the admin room via the Matrix API |
||||
|
TXN_ID="backup-$(date +%s)" |
||||
|
curl -sSf -o /dev/null -X PUT \ |
||||
|
-H "Authorization: Bearer ${ADMIN_ACCESS_TOKEN}" \ |
||||
|
-H "Content-Type: application/json" \ |
||||
|
-d '{"msgtype":"m.text","body":"!admin server backup-database"}' \ |
||||
|
"${MATRIX_URL}/_matrix/client/v3/rooms/${ADMIN_ROOM_ID}/send/m.room.message/${TXN_ID}" |
||||
|
|
||||
|
echo "Backup command sent. Waiting for Tuwunel to complete it..." |
||||
|
|
||||
|
# Wait for new/updated files to appear under the backup source (max 5 minutes) |
||||
|
TIMEOUT="${BACKUP_TIMEOUT}" |
||||
|
ELAPSED=0 |
||||
|
INTERVAL="${BACKUP_CHECK_INTERVAL}" |
||||
|
while [ "$ELAPSED" -lt "$TIMEOUT" ]; do |
||||
|
if find "${BACKUP_SOURCE}" -newer "${MARKER}" -type f 2>/dev/null | grep -q .; then |
||||
|
echo "Backup files detected after ${ELAPSED}s." |
||||
|
break |
||||
|
fi |
||||
|
sleep "$INTERVAL" |
||||
|
ELAPSED=$((ELAPSED + INTERVAL)) |
||||
|
done |
||||
|
|
||||
|
if [ "$ELAPSED" -ge "$TIMEOUT" ]; then |
||||
|
echo "ERROR: Timed out waiting for backup after ${TIMEOUT}s." >&2 |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Find the latest numbered backup directory created by RocksDB BackupEngine |
||||
|
LATEST_NUM=$(ls -1 "${BACKUP_SOURCE}/private" | grep -E '^[0-9]+$' | sort -n | tail -1) |
||||
|
if [ -z "$LATEST_NUM" ]; then |
||||
|
echo "ERROR: No numbered backup directory found in ${BACKUP_SOURCE}/private." >&2 |
||||
|
exit 1 |
||||
|
fi |
||||
|
echo "Processing RocksDB backup #${LATEST_NUM} into restore-ready format..." |
||||
|
|
||||
|
# 1. Copy and rename SST files from shared_checksum/: |
||||
|
# ######_sXXXXXXXX.sst → ######.sst |
||||
|
SHARED="${BACKUP_SOURCE}/shared_checksum" |
||||
|
if [ -d "${SHARED}" ]; then |
||||
|
for sst in "${SHARED}"/*.sst; do |
||||
|
[ -f "$sst" ] || continue |
||||
|
dest=$(basename "$sst" | sed 's/_s.*/.sst/') |
||||
|
cp "$sst" "${STAGING}/${dest}" |
||||
|
done |
||||
|
fi |
||||
|
|
||||
|
# 2. Copy all files from the latest numbered directory (CURRENT, MANIFEST, OPTIONS, ...) |
||||
|
find "${BACKUP_SOURCE}/private/${LATEST_NUM}" -maxdepth 1 -type f | while read -r f; do |
||||
|
cp "$f" "${STAGING}/" |
||||
|
done |
||||
|
|
||||
|
# Archive the restore-ready staging directory |
||||
|
BACKUP_DATE=$(date +%Y-%m-%d_%H-%M-%S) |
||||
|
ARCHIVE="${BACKUP_DEST}/${BACKUP_DATE}.tar.gz" |
||||
|
|
||||
|
echo "Archiving restore-ready backup to ${ARCHIVE}..." |
||||
|
tar -czf "${ARCHIVE}" -C "${STAGING}" . |
||||
|
echo "Backup archived ($(du -sh "${ARCHIVE}" | cut -f1))." |
||||
|
|
||||
|
# Apply retention policy |
||||
|
if [ "${BACKUP_RETENTION}" -gt 0 ] && ls "${BACKUP_DEST}"/*.tar.gz > /dev/null 2>&1; then |
||||
|
echo "Applying retention policy: keeping last ${BACKUP_RETENTION} backups." |
||||
|
ls -1t "${BACKUP_DEST}"/*.tar.gz | tail -n "+$((BACKUP_RETENTION + 1))" | while read -r old; do |
||||
|
echo "Removing old backup: ${old}" |
||||
|
rm -f "${old}" |
||||
|
done |
||||
|
fi |
||||
|
|
||||
|
echo "Backup completed successfully." |
||||
@ -0,0 +1,4 @@ |
|||||
|
FROM quay.io/centos/centos:stream10 |
||||
|
|
||||
|
RUN dnf install -y curl jq \ |
||||
|
&& dnf clean all |
||||
@ -0,0 +1,34 @@ |
|||||
|
server { |
||||
|
listen 127.0.0.1:${ELEMENT_WEB_PORT}; |
||||
|
listen [::1]:${ELEMENT_WEB_PORT}; |
||||
|
server_name localhost; |
||||
|
|
||||
|
root /usr/share/nginx/html; |
||||
|
index index.html; |
||||
|
|
||||
|
# Set no-cache for the version, config, i18n, and index.html |
||||
|
# so that browsers always check for a new copy of Element Web. |
||||
|
# NB http://your-domain/ and http://your-domain/? are also covered by this |
||||
|
|
||||
|
location = /index.html { |
||||
|
add_header Cache-Control "no-cache"; |
||||
|
} |
||||
|
location = /version { |
||||
|
add_header Cache-Control "no-cache"; |
||||
|
} |
||||
|
location /i18n/ { |
||||
|
add_header Cache-Control "no-cache"; |
||||
|
} |
||||
|
|
||||
|
# covers config.json and config.hostname.json requests as it is prefix. |
||||
|
location /config { |
||||
|
root /tmp/element-web-config; |
||||
|
add_header Cache-Control "no-cache"; |
||||
|
} |
||||
|
location /modules { |
||||
|
alias /modules; |
||||
|
} |
||||
|
# redirect server error pages to the static page /50x.html |
||||
|
# |
||||
|
error_page 500 502 503 504 /50x.html; |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
{ |
||||
|
"default_server_config": { |
||||
|
"m.homeserver": { |
||||
|
"base_url": "http://matrix.example.com", |
||||
|
"server_name": "example.com" |
||||
|
} |
||||
|
}, |
||||
|
"brand": "Element", |
||||
|
"disable_guests": true, |
||||
|
"default_country_code": "FR" |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
## |
||||
|
## Matrix Configuration Environment Variables (Tuwunel) |
||||
|
## |
||||
|
## IMPORTANT: server_name cannot be changed after the first start. |
||||
|
## Prefer a root domain (e.g. example.com) so users get @user:example.com handles. |
||||
|
## The server itself can be accessed at a subdomain (e.g. matrix.example.com) via |
||||
|
## a .well-known delegation. |
||||
|
## |
||||
|
|
||||
|
# Server identity |
||||
|
TUWUNEL_SERVER_NAME=example.com |
||||
|
|
||||
|
# Bind to localhost only - Traefik handles the public-facing TLS |
||||
|
TUWUNEL_ADDRESS=127.0.0.1 |
||||
|
TUWUNEL_PORT=6167 |
||||
|
|
||||
|
# Database storage path (inside the container) |
||||
|
TUWUNEL_DATABASE_PATH=/var/lib/tuwunel/db |
||||
|
|
||||
|
# Registration |
||||
|
# Set a token to restrict who can register. |
||||
|
TUWUNEL_ALLOW_REGISTRATION=true |
||||
|
TUWUNEL_REGISTRATION_TOKEN=changeme |
||||
|
|
||||
|
# First user to register is automatically granted admin. |
||||
|
TUWUNEL_GRANT_ADMIN_TO_FIRST_USER=true |
||||
|
|
||||
|
# Federation (disabled for a private home server) |
||||
|
TUWUNEL_ALLOW_FEDERATION=false |
||||
|
|
||||
|
# Trusted servers for public key queries (used even without full federation) |
||||
|
TUWUNEL_TRUSTED_SERVERS=["matrix.org"] |
||||
|
|
||||
|
# Tell Tuwunel to trust the X-Forwarded-For header set by Traefik |
||||
|
TUWUNEL_IP_SOURCE=rightmost_x_forwarded_for |
||||
|
|
||||
|
# Log level |
||||
|
TUWUNEL_LOG=info |
||||
|
|
||||
|
# Media Storage path and options |
||||
|
TUWUNEL_STORAGE_PROVIDER__MEDIA__LOCAL__BASE_PATH=/var/lib/tuwunel/media |
||||
|
TUWUNEL_STORAGE_PROVIDER__MEDIA__LOCAL__DELETE_EMPTY_DIRECTORIES=true |
||||
|
TUWUNEL_STORAGE_PROVIDER__MEDIA__LOCAL__STARTUP_CHECK=true |
||||
|
|
||||
|
## |
||||
|
## First-run initialization (matrix-init) |
||||
|
## These variables are used once by matrix-init to create the admin user and |
||||
|
## discover the admin room. The generated credentials are stored in |
||||
|
## /var/lib/quadlets/matrix/tuwunel-backup.env (never edit that file manually). |
||||
|
## |
||||
|
|
||||
|
# Username and password for the initial admin account. |
||||
|
MATRIX_INIT_ADMIN_USER=admin |
||||
|
MATRIX_INIT_ADMIN_PASSWORD=changeme |
||||
|
|
||||
|
## |
||||
|
## Backup configuration |
||||
|
## |
||||
|
|
||||
|
# Path where Tuwunel stores RocksDB online backups (inside the container). |
||||
|
# Each call to '!admin server backup-database' creates an incremental backup here. |
||||
|
TUWUNEL_DATABASE_BACKUP_PATH=/var/lib/tuwunel/backup |
||||
|
|
||||
|
# Number of dated backup archives to keep on virtiofs (0 = keep all). |
||||
|
MATRIX_BACKUP_RETENTION=7 |
||||
|
|
||||
|
# NOTE: MATRIX_BACKUP_ACCESS_TOKEN and MATRIX_BACKUP_ROOM_ID are auto-generated |
||||
|
# by matrix-init into /var/lib/quadlets/matrix/tuwunel-backup.env. Do not set |
||||
|
# them here. |
||||
@ -0,0 +1,85 @@ |
|||||
|
#!/bin/sh |
||||
|
set -eu |
||||
|
|
||||
|
MATRIX_URL="http://127.0.0.1:${TUWUNEL_PORT:-6167}" |
||||
|
OUTPUT_FILE="/output/tuwunel-backup.env" |
||||
|
|
||||
|
ADMIN_USER="${MATRIX_INIT_ADMIN_USER:-admin}" |
||||
|
ADMIN_PASSWORD="${MATRIX_INIT_ADMIN_PASSWORD}" |
||||
|
REGISTRATION_TOKEN="${TUWUNEL_REGISTRATION_TOKEN}" |
||||
|
SERVER_NAME="${TUWUNEL_SERVER_NAME}" |
||||
|
|
||||
|
# Wait for Tuwunel to accept connections |
||||
|
echo "Waiting for Tuwunel at ${MATRIX_URL}..." |
||||
|
TIMEOUT=120; ELAPSED=0 |
||||
|
while [ "$ELAPSED" -lt "$TIMEOUT" ]; do |
||||
|
if curl -sf "${MATRIX_URL}/_matrix/client/versions" > /dev/null 2>&1; then |
||||
|
echo "Tuwunel is ready." |
||||
|
break |
||||
|
fi |
||||
|
sleep 5; ELAPSED=$((ELAPSED + 5)) |
||||
|
done |
||||
|
[ "$ELAPSED" -lt "$TIMEOUT" ] || { echo "ERROR: Tuwunel not ready after ${TIMEOUT}s." >&2; exit 1; } |
||||
|
|
||||
|
# Try login first in case the admin user already exists (e.g. partial previous run) |
||||
|
echo "Attempting login as ${ADMIN_USER}..." |
||||
|
LOGIN_RESPONSE=$(curl -s -X POST \ |
||||
|
-H "Content-Type: application/json" \ |
||||
|
-d "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"${ADMIN_USER}\"},\"password\":\"${ADMIN_PASSWORD}\"}" \ |
||||
|
"${MATRIX_URL}/_matrix/client/v3/login") |
||||
|
|
||||
|
ACCESS_TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.access_token // empty') |
||||
|
|
||||
|
if [ -z "$ACCESS_TOKEN" ]; then |
||||
|
echo "Login failed; registering ${ADMIN_USER} (TUWUNEL_GRANT_ADMIN_TO_FIRST_USER=true)..." |
||||
|
|
||||
|
# Step 1: get the UIAA session ID (server returns 401 with session in body) |
||||
|
UIAA_RESPONSE=$(curl -s -X POST \ |
||||
|
-H "Content-Type: application/json" \ |
||||
|
-d "{\"username\":\"${ADMIN_USER}\",\"password\":\"${ADMIN_PASSWORD}\"}" \ |
||||
|
"${MATRIX_URL}/_matrix/client/v3/register?kind=user") |
||||
|
|
||||
|
SESSION=$(echo "$UIAA_RESPONSE" | jq -r '.session // empty') |
||||
|
[ -n "$SESSION" ] || { echo "ERROR: Could not obtain UIAA session." >&2; exit 1; } |
||||
|
|
||||
|
# Step 2: register using the registration token |
||||
|
REGISTER_RESPONSE=$(curl -s -X POST \ |
||||
|
-H "Content-Type: application/json" \ |
||||
|
-d "{\"username\":\"${ADMIN_USER}\",\"password\":\"${ADMIN_PASSWORD}\",\"auth\":{\"type\":\"m.login.registration_token\",\"token\":\"${REGISTRATION_TOKEN}\",\"session\":\"${SESSION}\"}}" \ |
||||
|
"${MATRIX_URL}/_matrix/client/v3/register?kind=user") |
||||
|
|
||||
|
ACCESS_TOKEN=$(echo "$REGISTER_RESPONSE" | jq -r '.access_token // empty') |
||||
|
[ -n "$ACCESS_TOKEN" ] || { echo "ERROR: Registration failed: $(echo "$REGISTER_RESPONSE" | jq -r '.error // .')" >&2; exit 1; } |
||||
|
echo "User ${ADMIN_USER} registered and granted admin." |
||||
|
else |
||||
|
echo "Logged in as ${ADMIN_USER}." |
||||
|
fi |
||||
|
|
||||
|
# Find the admin room: the one that contains the server bot (@tuwunel:<server>) |
||||
|
echo "Looking for admin room..." |
||||
|
JOINED_ROOMS=$(curl -sf \ |
||||
|
-H "Authorization: Bearer ${ACCESS_TOKEN}" \ |
||||
|
"${MATRIX_URL}/_matrix/client/v3/joined_rooms" | jq -r '.joined_rooms[]') |
||||
|
|
||||
|
ADMIN_ROOM_ID="" |
||||
|
for ROOM_ID in $JOINED_ROOMS; do |
||||
|
MEMBERS=$(curl -sf \ |
||||
|
-H "Authorization: Bearer ${ACCESS_TOKEN}" \ |
||||
|
"${MATRIX_URL}/_matrix/client/v3/rooms/${ROOM_ID}/joined_members" 2>/dev/null || echo '{}') |
||||
|
# The Tuwunel admin bot is @tuwunel:<server_name> |
||||
|
if echo "$MEMBERS" | jq -e ".joined | (has(\"@tuwunel:${SERVER_NAME}\") or has(\"@conduit:${SERVER_NAME}\"))" > /dev/null 2>&1; then |
||||
|
ADMIN_ROOM_ID="$ROOM_ID" |
||||
|
break |
||||
|
fi |
||||
|
done |
||||
|
|
||||
|
[ -n "$ADMIN_ROOM_ID" ] || { echo "ERROR: Admin room not found. Is the server bot in a room with ${ADMIN_USER}?" >&2; exit 1; } |
||||
|
echo "Admin room: ${ADMIN_ROOM_ID}" |
||||
|
|
||||
|
# Write generated credentials to output (picked up by ExecStartPost) |
||||
|
cat > "${OUTPUT_FILE}" << EOF |
||||
|
MATRIX_BACKUP_ACCESS_TOKEN=${ACCESS_TOKEN} |
||||
|
MATRIX_BACKUP_ROOM_ID=${ADMIN_ROOM_ID} |
||||
|
EOF |
||||
|
|
||||
|
echo "Initialization complete. Credentials written to ${OUTPUT_FILE}." |
||||
@ -0,0 +1,13 @@ |
|||||
|
#!/bin/bash |
||||
|
set -Eeuo pipefail |
||||
|
|
||||
|
# Find the latest restore-ready backup archive |
||||
|
LATEST=$(ls -1t "${BACKUP_SOURCE}"/*.tar.gz 2>/dev/null | head -1 || true) |
||||
|
if [ -z "$LATEST" ]; then |
||||
|
echo "No backup archive found in ${BACKUP_SOURCE}, starting with a fresh database." |
||||
|
exit 0 |
||||
|
fi |
||||
|
|
||||
|
echo "Restoring database from ${LATEST}..." |
||||
|
tar -xzf "${LATEST}" -C "${DB_DEST}" |
||||
|
echo "Restore completed. Database ready at ${DB_DEST}." |
||||
@ -0,0 +1,50 @@ |
|||||
|
[Unit] |
||||
|
Description=Matrix Homeserver - Database Backup |
||||
|
Documentation=https://matrix-construct.github.io/tuwunel/maintenance.html |
||||
|
After=network.target matrix-homeserver.service var-lib-virtiofs-data.mount matrix-tools-build.service |
||||
|
Requires=matrix-homeserver.service var-lib-virtiofs-data.mount |
||||
|
Wants=matrix-tools-build.service |
||||
|
|
||||
|
# Only run if the homeserver has been configured |
||||
|
ConditionPathExists=/etc/quadlets/matrix/tuwunel.env |
||||
|
|
||||
|
# Only run once backup credentials have been generated by matrix-init |
||||
|
ConditionPathExists=/var/lib/virtiofs/data/matrix/tuwunel-backup.env |
||||
|
|
||||
|
PartOf=matrix.target |
||||
|
|
||||
|
[Container] |
||||
|
ContainerName=matrix-backup-job |
||||
|
Image=localhost/matrix-tools:latest |
||||
|
|
||||
|
# Network (to reach Tuwunel API on localhost) |
||||
|
Network=host |
||||
|
|
||||
|
# No need for root privileges |
||||
|
User=10031 |
||||
|
Group=10000 |
||||
|
|
||||
|
# TUWUNEL_* vars (port, server name, backup path, retention) |
||||
|
EnvironmentFile=/etc/quadlets/matrix/tuwunel.env |
||||
|
# MATRIX_BACKUP_ACCESS_TOKEN and MATRIX_BACKUP_ROOM_ID (generated by matrix-init) |
||||
|
EnvironmentFile=/var/lib/virtiofs/data/matrix/tuwunel-backup.env |
||||
|
|
||||
|
# Custom backup script |
||||
|
Entrypoint=/usr/local/bin/backup.sh |
||||
|
Volume=/etc/quadlets/matrix/backup.sh:/usr/local/bin/backup.sh:z,ro |
||||
|
|
||||
|
# RocksDB backup directory (read-only - Tuwunel writes here, we archive it) |
||||
|
Environment=BACKUP_SOURCE=/var/lib/tuwunel/backup |
||||
|
Volume=/var/lib/quadlets/matrix/tuwunel/backup-staging:/var/lib/tuwunel/backup:z,ro |
||||
|
|
||||
|
# Backup archive destination on virtiofs |
||||
|
Environment=BACKUP_DEST=/backup |
||||
|
Volume=/var/lib/virtiofs/data/matrix/tuwunel/backup:/backup:z |
||||
|
|
||||
|
[Service] |
||||
|
Restart=no |
||||
|
TimeoutStartSec=infinity |
||||
|
Type=oneshot |
||||
|
|
||||
|
# Make sure the staging area is cleaned up after the backup job completes (successfully or not) |
||||
|
ExecStartPost=find /var/lib/quadlets/matrix/tuwunel/backup-staging -mindepth 1 -delete |
||||
@ -0,0 +1,11 @@ |
|||||
|
[Unit] |
||||
|
Description=Matrix Homeserver - Database Backup Timer |
||||
|
Documentation=https://matrix-construct.github.io/tuwunel/maintenance.html |
||||
|
PartOf=matrix.target |
||||
|
|
||||
|
[Timer] |
||||
|
OnCalendar=daily |
||||
|
RandomizedDelaySec=15min |
||||
|
|
||||
|
[Install] |
||||
|
WantedBy=matrix.target |
||||
@ -0,0 +1,40 @@ |
|||||
|
[Unit] |
||||
|
Description=Matrix Homeserver (Tuwunel) |
||||
|
Documentation=https://matrix-construct.github.io/tuwunel/ |
||||
|
After=local-fs.target network.target var-lib-virtiofs-data.mount matrix-restore.service |
||||
|
Requires=var-lib-virtiofs-data.mount matrix-restore.service |
||||
|
Before=matrix.target |
||||
|
|
||||
|
# Only run if the homeserver has been configured |
||||
|
ConditionPathExists=/etc/quadlets/matrix/tuwunel.env |
||||
|
|
||||
|
PartOf=matrix.target |
||||
|
|
||||
|
[Container] |
||||
|
ContainerName=matrix-homeserver |
||||
|
Image=matrix-homeserver.image |
||||
|
AutoUpdate=registry |
||||
|
|
||||
|
# No need for root privileges |
||||
|
User=10031 |
||||
|
Group=10000 |
||||
|
|
||||
|
# Network |
||||
|
Network=host |
||||
|
|
||||
|
# Environment |
||||
|
EnvironmentFile=/etc/quadlets/matrix/tuwunel.env |
||||
|
|
||||
|
# Storage: database (local) and media files (virtiofs) |
||||
|
Volume=/var/lib/quadlets/matrix/tuwunel/db:/var/lib/tuwunel/db:z |
||||
|
Volume=/var/lib/virtiofs/data/matrix/tuwunel/media:/var/lib/tuwunel/media:z |
||||
|
Volume=/var/lib/quadlets/matrix/tuwunel/backup-staging:/var/lib/tuwunel/backup:z |
||||
|
|
||||
|
[Service] |
||||
|
Restart=always |
||||
|
RestartSec=10 |
||||
|
TimeoutStartSec=120 |
||||
|
TimeoutStopSec=30 |
||||
|
|
||||
|
[Install] |
||||
|
WantedBy=matrix.target |
||||
@ -0,0 +1,8 @@ |
|||||
|
[Unit] |
||||
|
Description=podman pull ghcr.io/matrix-construct/tuwunel |
||||
|
Documentation=https://matrix-construct.github.io/tuwunel/ |
||||
|
|
||||
|
ConditionPathExists=/etc/quadlets/matrix/tuwunel.env |
||||
|
|
||||
|
[Image] |
||||
|
Image=ghcr.io/matrix-construct/tuwunel:latest |
||||
@ -0,0 +1,43 @@ |
|||||
|
[Unit] |
||||
|
Description=Matrix Homeserver - First-run initialization |
||||
|
Documentation=https://matrix-construct.github.io/tuwunel/ |
||||
|
After=local-fs.target network.target matrix-homeserver.service matrix-tools-build.service |
||||
|
Requires=matrix-homeserver.service |
||||
|
Wants=matrix-tools-build.service |
||||
|
|
||||
|
# Only run if the homeserver has been configured |
||||
|
ConditionPathExists=/etc/quadlets/matrix/tuwunel.env |
||||
|
|
||||
|
# Skip once the backup credentials have been generated |
||||
|
ConditionPathExists=!/var/lib/virtiofs/data/matrix/tuwunel-backup.env |
||||
|
|
||||
|
PartOf=matrix.target |
||||
|
|
||||
|
[Container] |
||||
|
ContainerName=matrix-init-job |
||||
|
Image=localhost/matrix-tools:latest |
||||
|
|
||||
|
User=10031 |
||||
|
Group=10000 |
||||
|
|
||||
|
Network=host |
||||
|
|
||||
|
EnvironmentFile=/etc/quadlets/matrix/tuwunel.env |
||||
|
|
||||
|
# Custom initialization script (generates initial admin + backup job credentials) |
||||
|
Entrypoint=/usr/local/bin/init.sh |
||||
|
Volume=/etc/quadlets/matrix/init.sh:/usr/local/bin/init.sh:z,ro |
||||
|
|
||||
|
# Writable tmpfs output directory (copied to persistent storage by ExecStartPost) |
||||
|
Volume=/run/quadlets/matrix/matrix-init:/output:z |
||||
|
|
||||
|
[Service] |
||||
|
Restart=no |
||||
|
TimeoutStartSec=infinity |
||||
|
Type=oneshot |
||||
|
|
||||
|
# Persist the generated credentials to durable storage after the container exits |
||||
|
ExecStartPost=install -m 600 -o root -g root /run/quadlets/matrix/matrix-init/tuwunel-backup.env /var/lib/virtiofs/data/matrix/tuwunel-backup.env |
||||
|
|
||||
|
[Install] |
||||
|
WantedBy=matrix.target |
||||
@ -0,0 +1,42 @@ |
|||||
|
[Unit] |
||||
|
Description=Matrix Homeserver - Database Restore |
||||
|
Documentation=https://matrix-construct.github.io/tuwunel/maintenance.html |
||||
|
After=local-fs.target var-lib-virtiofs-data.mount matrix-tools-build.service |
||||
|
Requires=var-lib-virtiofs-data.mount |
||||
|
Wants=matrix-tools-build.service |
||||
|
Before=matrix-homeserver.service |
||||
|
|
||||
|
# Only run if the homeserver has been configured |
||||
|
ConditionPathExists=/etc/quadlets/matrix/tuwunel.env |
||||
|
|
||||
|
# Only restore when no RocksDB database exists yet |
||||
|
ConditionPathExists=!/var/lib/quadlets/matrix/tuwunel/db/CURRENT |
||||
|
# And there is at least one backup archive available to restore from |
||||
|
ConditionPathExistsGlob=/var/lib/virtiofs/data/matrix/tuwunel/backup/*.tar.gz |
||||
|
|
||||
|
PartOf=matrix.target |
||||
|
|
||||
|
[Container] |
||||
|
ContainerName=matrix-restore-job |
||||
|
Image=localhost/matrix-tools:latest |
||||
|
|
||||
|
# No need for root privileges |
||||
|
User=10031 |
||||
|
Group=10000 |
||||
|
|
||||
|
# Restore script |
||||
|
Entrypoint=/usr/local/bin/restore.sh |
||||
|
Volume=/etc/quadlets/matrix/restore.sh:/usr/local/bin/restore.sh:z,ro |
||||
|
|
||||
|
# Latest backup archive from virtiofs (read-only) |
||||
|
Environment=BACKUP_SOURCE=/backup |
||||
|
Volume=/var/lib/virtiofs/data/matrix/tuwunel/backup:/backup:z,ro |
||||
|
|
||||
|
# Database destination |
||||
|
Environment=DB_DEST=/var/lib/tuwunel/db |
||||
|
Volume=/var/lib/quadlets/matrix/tuwunel/db:/var/lib/tuwunel/db:z |
||||
|
|
||||
|
[Service] |
||||
|
Restart=no |
||||
|
TimeoutStartSec=infinity |
||||
|
Type=oneshot |
||||
@ -0,0 +1,9 @@ |
|||||
|
[Unit] |
||||
|
Description=Matrix tools image build (backup, restore, init scripts) |
||||
|
Wants=network-online.target centos-stream10-image.service |
||||
|
After=network-online.target centos-stream10-image.service |
||||
|
|
||||
|
[Build] |
||||
|
File=/etc/quadlets/matrix/container/Containerfile |
||||
|
ImageTag=localhost/matrix-tools:latest |
||||
|
SetWorkingDirectory=/etc/quadlets/matrix/container |
||||
@ -0,0 +1,46 @@ |
|||||
|
[Unit] |
||||
|
Description=Matrix Element Web Client |
||||
|
Documentation=https://element.io/ |
||||
|
After=local-fs.target network.target |
||||
|
Before=matrix.target |
||||
|
|
||||
|
ConditionPathExists=/etc/quadlets/matrix/element-web/config.json |
||||
|
|
||||
|
PartOf=matrix.target |
||||
|
|
||||
|
[Container] |
||||
|
ContainerName=matrix-web |
||||
|
Image=matrix-web.image |
||||
|
AutoUpdate=registry |
||||
|
|
||||
|
# Network |
||||
|
Network=host |
||||
|
|
||||
|
# Port - must match ELEMENT_WEB_PORT below |
||||
|
Environment=ELEMENT_WEB_PORT=8080 |
||||
|
|
||||
|
# Config file (must exist before starting) |
||||
|
Volume=/etc/quadlets/matrix/element-web/config.json:/app/config.json:z,ro |
||||
|
Volume=/etc/quadlets/matrix/element-web/nginx-default.conf.template:/etc/nginx/templates/default.conf.template:z,ro |
||||
|
|
||||
|
# UID/GID mapping to map the nginx (101) user & group inside the container to arbitrary user 10031 / group 10000 on the host |
||||
|
UIDMap=0:1000000:65535 |
||||
|
UIDMap=+101:10031:1 |
||||
|
GIDMap=0:1000000:65535 |
||||
|
GIDMap=+101:10000:1 |
||||
|
|
||||
|
# Health check |
||||
|
HealthCmd=wget -q -O /dev/null http://127.0.0.1:8080/ |
||||
|
HealthInterval=30s |
||||
|
HealthTimeout=10s |
||||
|
HealthStartPeriod=30s |
||||
|
HealthRetries=3 |
||||
|
|
||||
|
[Service] |
||||
|
Restart=always |
||||
|
RestartSec=10 |
||||
|
TimeoutStartSec=120 |
||||
|
TimeoutStopSec=30 |
||||
|
|
||||
|
[Install] |
||||
|
WantedBy=matrix.target |
||||
@ -0,0 +1,8 @@ |
|||||
|
[Unit] |
||||
|
Description=podman pull docker.io/vectorim/element-web |
||||
|
Documentation=https://element.io/ |
||||
|
|
||||
|
ConditionPathExists=/etc/quadlets/matrix/element-web/config.json |
||||
|
|
||||
|
[Image] |
||||
|
Image=docker.io/vectorim/element-web:latest |
||||
@ -0,0 +1,11 @@ |
|||||
|
[Unit] |
||||
|
Description=Matrix Service Target |
||||
|
Documentation=man:systemd.target(5) |
||||
|
Requires=matrix-homeserver.service matrix-web.service matrix-init.service matrix-backup.timer |
||||
|
After=matrix-homeserver.service matrix-web.service matrix-init.service |
||||
|
Before=matrix-backup.timer |
||||
|
|
||||
|
AllowIsolate=yes |
||||
|
|
||||
|
[Install] |
||||
|
WantedBy=multi-user.target |
||||
@ -0,0 +1,25 @@ |
|||||
|
http: |
||||
|
routers: |
||||
|
matrix: |
||||
|
rule: "Host(`matrix`)" |
||||
|
entryPoints: |
||||
|
- http |
||||
|
middlewares: |
||||
|
service: "matrix" |
||||
|
matrix-web: |
||||
|
# Element Web client |
||||
|
rule: "Host(`matrix-chat`)" |
||||
|
entryPoints: |
||||
|
- http |
||||
|
middlewares: |
||||
|
service: "matrix-web" |
||||
|
services: |
||||
|
matrix: |
||||
|
loadBalancer: |
||||
|
servers: |
||||
|
- url: "http://127.0.0.1:6167" |
||||
|
passHostHeader: true |
||||
|
matrix-web: |
||||
|
loadBalancer: |
||||
|
servers: |
||||
|
- url: "http://127.0.0.1:8080" |
||||
@ -0,0 +1,9 @@ |
|||||
|
variant: fcos |
||||
|
version: 1.4.0 |
||||
|
passwd: |
||||
|
users: |
||||
|
- name: matrix |
||||
|
uid: 10031 |
||||
|
gecos: Matrix |
||||
|
home_dir: /var/lib/quadlets/matrix |
||||
|
primary_group: itix-svc |
||||
@ -0,0 +1,8 @@ |
|||||
|
d /var/lib/quadlets/matrix/tuwunel 0700 10031 10000 - |
||||
|
d /var/lib/quadlets/matrix/tuwunel/db 0700 10031 10000 - |
||||
|
d /var/lib/quadlets/matrix/tuwunel/backup-staging 0700 10031 10000 - |
||||
|
d /run/quadlets/matrix/matrix-init 0700 10031 10000 - |
||||
|
d$ /var/lib/virtiofs/data/matrix 0700 10031 10000 - |
||||
|
d$ /var/lib/virtiofs/data/matrix/tuwunel 0700 10031 10000 - |
||||
|
d$ /var/lib/virtiofs/data/matrix/tuwunel/media 0700 10031 10000 - |
||||
|
d$ /var/lib/virtiofs/data/matrix/tuwunel/backup 0700 10031 10000 - |
||||
Loading…
Reference in new issue