diff --git a/README.md b/README.md index aef0b5f..ac749d5 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A common Makefile-based tooling (`make install`, `make package`, `make pytest`, - [forgejo](cookbooks/forgejo/): self-hosted Git service (formerly Gitea), a lightweight GitHub/GitLab alternative, with PostgreSQL backend. - [keycloak](cookbooks/keycloak/): open source identity and access management server with PostgreSQL backend. - [lego](cookbooks/lego/): Let's Encrypt/ACME client for automatic SSL/TLS certificate management and renewal. +- [matrix](cookbooks/matrix/): self-hosted Matrix homeserver (Tuwunel) with Element Web client, automated backups and restore. - [miniflux](cookbooks/miniflux/): minimalist RSS/Atom feed reader with PostgreSQL backend. - [nextcloud](cookbooks/nextcloud/): self-hosted file sync and share platform with all its dependencies, handles automated upgrades. - [nftables](cookbooks/nftables/): system-wide nftables firewall rules, composable via hooks from other cookbooks. diff --git a/cookbooks/matrix/Makefile b/cookbooks/matrix/Makefile new file mode 100644 index 0000000..2b5b417 --- /dev/null +++ b/cookbooks/matrix/Makefile @@ -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 diff --git a/cookbooks/matrix/README.md b/cookbooks/matrix/README.md new file mode 100644 index 0000000..ad78b1c --- /dev/null +++ b/cookbooks/matrix/README.md @@ -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:`). | +| `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 +``` diff --git a/cookbooks/matrix/config/backup.sh b/cookbooks/matrix/config/backup.sh new file mode 100755 index 0000000..8a00cc0 --- /dev/null +++ b/cookbooks/matrix/config/backup.sh @@ -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." diff --git a/cookbooks/matrix/config/container/Containerfile b/cookbooks/matrix/config/container/Containerfile new file mode 100644 index 0000000..8af5386 --- /dev/null +++ b/cookbooks/matrix/config/container/Containerfile @@ -0,0 +1,4 @@ +FROM quay.io/centos/centos:stream10 + +RUN dnf install -y curl jq \ + && dnf clean all diff --git a/cookbooks/matrix/config/element-web/nginx-default.conf.template b/cookbooks/matrix/config/element-web/nginx-default.conf.template new file mode 100644 index 0000000..a728851 --- /dev/null +++ b/cookbooks/matrix/config/element-web/nginx-default.conf.template @@ -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; +} diff --git a/cookbooks/matrix/config/examples/element-web/config.json b/cookbooks/matrix/config/examples/element-web/config.json new file mode 100644 index 0000000..a851ee3 --- /dev/null +++ b/cookbooks/matrix/config/examples/element-web/config.json @@ -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" +} diff --git a/cookbooks/matrix/config/examples/tuwunel.env b/cookbooks/matrix/config/examples/tuwunel.env new file mode 100644 index 0000000..76b6639 --- /dev/null +++ b/cookbooks/matrix/config/examples/tuwunel.env @@ -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. diff --git a/cookbooks/matrix/config/init.sh b/cookbooks/matrix/config/init.sh new file mode 100755 index 0000000..079d814 --- /dev/null +++ b/cookbooks/matrix/config/init.sh @@ -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:) +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: + 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}." diff --git a/cookbooks/matrix/config/restore.sh b/cookbooks/matrix/config/restore.sh new file mode 100755 index 0000000..c9cefe3 --- /dev/null +++ b/cookbooks/matrix/config/restore.sh @@ -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}." diff --git a/cookbooks/matrix/matrix-backup.container b/cookbooks/matrix/matrix-backup.container new file mode 100644 index 0000000..387a479 --- /dev/null +++ b/cookbooks/matrix/matrix-backup.container @@ -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 diff --git a/cookbooks/matrix/matrix-backup.timer b/cookbooks/matrix/matrix-backup.timer new file mode 100644 index 0000000..6259bf7 --- /dev/null +++ b/cookbooks/matrix/matrix-backup.timer @@ -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 diff --git a/cookbooks/matrix/matrix-homeserver.container b/cookbooks/matrix/matrix-homeserver.container new file mode 100644 index 0000000..b0a4821 --- /dev/null +++ b/cookbooks/matrix/matrix-homeserver.container @@ -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 diff --git a/cookbooks/matrix/matrix-homeserver.image b/cookbooks/matrix/matrix-homeserver.image new file mode 100644 index 0000000..9ed9f8d --- /dev/null +++ b/cookbooks/matrix/matrix-homeserver.image @@ -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 diff --git a/cookbooks/matrix/matrix-init.container b/cookbooks/matrix/matrix-init.container new file mode 100644 index 0000000..5752302 --- /dev/null +++ b/cookbooks/matrix/matrix-init.container @@ -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 diff --git a/cookbooks/matrix/matrix-restore.container b/cookbooks/matrix/matrix-restore.container new file mode 100644 index 0000000..e5d2ae6 --- /dev/null +++ b/cookbooks/matrix/matrix-restore.container @@ -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 diff --git a/cookbooks/matrix/matrix-tools.build b/cookbooks/matrix/matrix-tools.build new file mode 100644 index 0000000..2c7889e --- /dev/null +++ b/cookbooks/matrix/matrix-tools.build @@ -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 diff --git a/cookbooks/matrix/matrix-web.container b/cookbooks/matrix/matrix-web.container new file mode 100644 index 0000000..f92519f --- /dev/null +++ b/cookbooks/matrix/matrix-web.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 diff --git a/cookbooks/matrix/matrix-web.image b/cookbooks/matrix/matrix-web.image new file mode 100644 index 0000000..2622a7a --- /dev/null +++ b/cookbooks/matrix/matrix-web.image @@ -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 diff --git a/cookbooks/matrix/matrix.target b/cookbooks/matrix/matrix.target new file mode 100644 index 0000000..5ed9091 --- /dev/null +++ b/cookbooks/matrix/matrix.target @@ -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 diff --git a/cookbooks/matrix/other/traefik/matrix.yaml b/cookbooks/matrix/other/traefik/matrix.yaml new file mode 100644 index 0000000..9798225 --- /dev/null +++ b/cookbooks/matrix/other/traefik/matrix.yaml @@ -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" diff --git a/cookbooks/matrix/overlay.bu b/cookbooks/matrix/overlay.bu new file mode 100644 index 0000000..cfb703b --- /dev/null +++ b/cookbooks/matrix/overlay.bu @@ -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 diff --git a/cookbooks/matrix/tmpfiles.d/matrix.conf b/cookbooks/matrix/tmpfiles.d/matrix.conf new file mode 100644 index 0000000..9ba7b57 --- /dev/null +++ b/cookbooks/matrix/tmpfiles.d/matrix.conf @@ -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 -