Compare commits
2 Commits
2dcec43f7c
...
6224fcfcea
| Author | SHA1 | Date |
|---|---|---|
|
|
6224fcfcea | 1 month ago |
|
|
a93a2cfc65 | 1 month ago |
24 changed files with 797 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 @@ |
|||
ELEMENT_WEB_PORT=8081 |
|||
@ -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,49 @@ |
|||
[Unit] |
|||
Description=Matrix Element Web Client |
|||
Documentation=https://element.io/ |
|||
After=local-fs.target network.target |
|||
Before=matrix.target |
|||
|
|||
# Only start if Element Web client has been configured |
|||
ConditionPathExists=/etc/quadlets/matrix/element-web/config.json |
|||
ConditionPathExists=/etc/quadlets/matrix/element-web.env |
|||
|
|||
PartOf=matrix.target |
|||
|
|||
[Container] |
|||
ContainerName=matrix-web |
|||
Image=matrix-web.image |
|||
AutoUpdate=registry |
|||
|
|||
# Network |
|||
Network=host |
|||
|
|||
# Source configuration into environment variables for the container |
|||
EnvironmentFile=/etc/quadlets/matrix/element-web.env |
|||
|
|||
# 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:${ELEMENT_WEB_PORT}/ |
|||
HealthInterval=30s |
|||
HealthTimeout=10s |
|||
HealthStartPeriod=30s |
|||
HealthRetries=3 |
|||
|
|||
[Service] |
|||
Restart=always |
|||
RestartSec=10 |
|||
TimeoutStartSec=120 |
|||
TimeoutStopSec=30 |
|||
EnvironmentFile=/etc/quadlets/matrix/element-web.env |
|||
|
|||
[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