Browse Source

add cookbook for Matrix

main
Nicolas Massé 1 month ago
parent
commit
a93a2cfc65
  1. 1
      README.md
  2. 12
      cookbooks/matrix/Makefile
  3. 168
      cookbooks/matrix/README.md
  4. 86
      cookbooks/matrix/config/backup.sh
  5. 4
      cookbooks/matrix/config/container/Containerfile
  6. 34
      cookbooks/matrix/config/element-web/nginx-default.conf.template
  7. 11
      cookbooks/matrix/config/examples/element-web/config.json
  8. 69
      cookbooks/matrix/config/examples/tuwunel.env
  9. 85
      cookbooks/matrix/config/init.sh
  10. 13
      cookbooks/matrix/config/restore.sh
  11. 50
      cookbooks/matrix/matrix-backup.container
  12. 11
      cookbooks/matrix/matrix-backup.timer
  13. 40
      cookbooks/matrix/matrix-homeserver.container
  14. 8
      cookbooks/matrix/matrix-homeserver.image
  15. 43
      cookbooks/matrix/matrix-init.container
  16. 42
      cookbooks/matrix/matrix-restore.container
  17. 9
      cookbooks/matrix/matrix-tools.build
  18. 46
      cookbooks/matrix/matrix-web.container
  19. 8
      cookbooks/matrix/matrix-web.image
  20. 11
      cookbooks/matrix/matrix.target
  21. 25
      cookbooks/matrix/other/traefik/matrix.yaml
  22. 9
      cookbooks/matrix/overlay.bu
  23. 8
      cookbooks/matrix/tmpfiles.d/matrix.conf

1
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.

12
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

168
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:<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
```

86
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."

4
cookbooks/matrix/config/container/Containerfile

@ -0,0 +1,4 @@
FROM quay.io/centos/centos:stream10
RUN dnf install -y curl jq \
&& dnf clean all

34
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;
}

11
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"
}

69
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.

85
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:<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}."

13
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}."

50
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

11
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

40
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

8
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

43
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

42
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

9
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

46
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

8
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

11
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

25
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"

9
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

8
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 -
Loading…
Cancel
Save