LibrePortal/containers/matrix/scripts/matrix_install_hooks.sh
librelad 65167463f9 fix(chat apps): tag every service so its IP actually substitutes
Installing rocketchat failed with

    invalid IPv4 address: ParseAddr("IP_DATA_2"): unable to parse IP

ipUpdateComposeTags allocates one IP per SERVICE_TAG_N annotation and fills
IP_TAG_i only where SERVICE_TAG_i exists. The four new apps tagged only their
primary service, so every sidecar — matrix's postgres, mattermost's postgres,
rocketchat's mongo, and fifteen of stoat's sixteen — kept a literal IP_DATA_n
in the deployed compose and docker refused to create the container.

Tag every service that carries an ipv4_address, index-aligned with its IP_TAG.
For stoat that also meant moving caddy from SERVICE_TAG_1 to _6 so the indices
line up with the IPs rather than the reading order.

mastodon had the same latent break (IP_TAG_2 and _3 untagged) and is fixed the
same way — it would have failed on first install for the same reason.

SERVICE_TAG carries the compose *key*, not container_name: 'libreportal app
restart <app> <service>' passes it to 'docker compose restart', which only
understands keys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:48:09 +01:00

225 lines
10 KiB
Bash

#!/bin/bash
# Matrix (Synapse + Element) install hooks.
#
# Synapse takes essentially no configuration from the environment — it reads
# homeserver.yaml and nothing else — so the real install work is done here:
# generate the signing key, write homeserver.yaml from the template with the
# server name and secrets filled in, write Element's config.json, and create the
# first admin account once the homeserver is answering.
matrix_install_pre()
{
local app_name="$1"
if ! appInstallCheckRequirements "$app_name" "$CFG_MATRIX_REQUIRES"; then
matrix=n
return 1
fi
}
# The homeserver's public host, read back out of the deployed compose after tag
# substitution has run. DOMAINSUBNAME_TAG_1 belongs to CFG_MATRIX_PORT_1 (the
# Synapse router), so this is the host that becomes server_name — deliberately
# not $host_setup, which for this two-host app points at Element instead.
_matrixServerName()
{
local app_name="$1"
tagsManagerGetTagContent "$containers_dir$app_name/docker-compose.yml" "DOMAINSUBNAME_TAG_1"
}
matrix_install_post_compose()
{
local app_name="$1"
local app_dir="$containers_dir$app_name"
local data_dir="$app_dir/data"
((menu_number++))
echo ""
echo "---- $menu_number. Generating the Synapse homeserver configuration"
echo ""
local server_name
server_name=$(_matrixServerName "$app_name")
if [[ -z "$server_name" ]]; then
isError "Could not determine the homeserver name from the compose file — aborting Synapse configuration."
isNotice "Check that CFG_MATRIX_PORT_1 is public and Traefik-managed, then reinstall."
return 1
fi
# Must match the password the compose handed to Postgres. Read it back from
# the deployed compose rather than from CFG_MATRIX_DB_PASSWORD_1: this hook
# runs after templating, so the compose is the settled value, and it stays
# correct even on an install whose config still holds the placeholder.
local db_password
db_password=$(tagsManagerGetTagContent "$app_dir/docker-compose.yml" "MATRIX_DB_PASSWORD_1_TAG")
if [[ -z "$db_password" || "$db_password" == "MATRIX_DB_PASSWORD_1_DATA" ]]; then
isError "Database password was not generated in the compose file — aborting Synapse configuration."
return 1
fi
local result
result=$(createFolders "loud" "$docker_install_user" "$data_dir" "$app_dir/element")
checkSuccess "Creating $app_name data folders"
# Synapse signs every federated event with this key, and a peer that has
# seen one key will reject events signed by a different one. So: generate it
# exactly once, and never regenerate it over an existing install.
#
# Upstream's `generate` command is used rather than hand-rolling the key
# file, because the format encodes a key ID that other servers cache. It
# also emits a homeserver.yaml and a log config, which we throw away in
# favour of the templates below.
if [[ ! -s "$data_dir/signing.key" ]]; then
local synapse_image
synapse_image=$(tagsManagerGetTagContent "$app_dir/docker-compose.yml" "MATRIX_VERSION_TAG")
synapse_image="matrixdotorg/synapse:${synapse_image:-latest}"
result=$(runFileOp docker run --rm \
-e SYNAPSE_SERVER_NAME="$server_name" \
-e SYNAPSE_REPORT_STATS=no \
-v "$data_dir":/data \
"$synapse_image" generate 2>&1)
checkSuccess "Generating the Synapse signing key with $synapse_image"
# `generate` names the key after the server; homeserver.yaml expects it
# at a fixed path so the file does not have to be renamed if the app is
# ever restored under a different name.
if [[ -f "$data_dir/$server_name.signing.key" ]]; then
result=$(runFileOp mv "$data_dir/$server_name.signing.key" "$data_dir/signing.key")
checkSuccess "Storing the signing key at data/signing.key"
fi
# Ours replace both of these.
result=$(runFileOp rm -f "$data_dir/homeserver.yaml" "$data_dir/$server_name.log.config")
checkSuccess "Discarding the generated config in favour of the LibrePortal template"
else
isNotice "An existing signing key was found — keeping it (regenerating would break federation)."
fi
if [[ ! -s "$data_dir/signing.key" ]]; then
isError "No signing key was produced — Synapse will not start. Check that the image could be pulled."
return 1
fi
result=$(copyResource "$app_name" "homeserver.yaml" "data" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying homeserver.yaml to $data_dir"
result=$(copyResource "$app_name" "log.config" "data" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying log.config to $data_dir"
local homeserver_file="$data_dir/homeserver.yaml"
# Three independent secrets, each generated fresh. registration_shared_secret
# can mint an account on this server, so it is as sensitive as an admin
# password — it is why homeserver.yaml is chmod 600 below.
local registration_secret macaroon_secret form_secret
registration_secret=$(openssl rand -hex 32)
macaroon_secret=$(openssl rand -hex 32)
form_secret=$(openssl rand -hex 32)
local enable_registration="false"
[[ "$CFG_MATRIX_ENABLE_REGISTRATION" == "true" ]] && enable_registration="true"
runFileOp sed -i "s|SYNAPSE_SERVER_NAME_PLACEHOLDER|$server_name|g" "$homeserver_file"
runFileOp sed -i "s|SYNAPSE_DB_PASSWORD_PLACEHOLDER|$db_password|g" "$homeserver_file"
runFileOp sed -i "s|SYNAPSE_REGISTRATION_SECRET_PLACEHOLDER|$registration_secret|g" "$homeserver_file"
runFileOp sed -i "s|SYNAPSE_MACAROON_SECRET_PLACEHOLDER|$macaroon_secret|g" "$homeserver_file"
runFileOp sed -i "s|SYNAPSE_FORM_SECRET_PLACEHOLDER|$form_secret|g" "$homeserver_file"
runFileOp sed -i "s|SYNAPSE_ENABLE_REGISTRATION_PLACEHOLDER|$enable_registration|g" "$homeserver_file"
checkSuccess "Writing homeserver.yaml (server_name=$server_name registration=$enable_registration)"
runFileOp chmod 600 "$homeserver_file" "$data_dir/signing.key"
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$data_dir"
checkSuccess "Restricting permissions on the Synapse secrets"
# Element is a static bundle; config.json is the only thing that makes it
# point at this homeserver rather than matrix.org.
result=$(copyResource "$app_name" "element-config.json" "element" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying Element configuration to $app_dir/element"
result=$(runFileOp mv "$app_dir/element/element-config.json" "$app_dir/element/config.json")
checkSuccess "Renaming Element configuration to config.json"
runFileOp sed -i "s|ELEMENT_HOMESERVER_PLACEHOLDER|$server_name|g" "$app_dir/element/config.json"
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$app_dir/element"
checkSuccess "Pointing Element at https://$server_name"
}
matrix_install_post_start()
{
local app_name="$1"
((menu_number++))
echo ""
echo "---- $menu_number. Creating the first Matrix admin account"
echo ""
# Synapse runs its database migrations on first boot, which on an empty
# Postgres takes appreciably longer than the container takes to start.
# /health answers only once it is actually serving.
#
# Probed with python rather than curl: the Synapse image is debian-slim with
# no curl or wget in it, but python is what Synapse itself runs on, so it is
# always there.
local attempts=0
while ((attempts < 60)); do
if runFileOp docker exec matrix-synapse python -c \
"import urllib.request; urllib.request.urlopen('http://localhost:8008/health', timeout=5)" >/dev/null 2>&1; then
break
fi
sleep 2
((attempts++))
done
if ((attempts >= 60)); then
isError "Synapse did not become ready in time — no admin account was created."
isNotice "Check 'docker logs matrix-synapse'. Once it is up, create the account with:"
isNotice " docker exec -it matrix-synapse register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008"
return 0
fi
local admin_user="${CFG_MATRIX_ADMIN_USERNAME:-admin}"
local admin_pass="${CFG_MATRIX_ADMIN_PASSWORD_1}"
if [[ -z "$admin_pass" || "$admin_pass" == RANDOMIZEDPASSWORD* ]]; then
isNotice "No admin password is set in matrix.config — skipping admin account creation."
return 0
fi
# Idempotent in practice: on a reinstall over existing data the account
# already exists and register_new_matrix_user fails with "User ID already
# taken", which is not worth failing the install over.
local result
result=$(runFileOp docker exec matrix-synapse register_new_matrix_user \
-u "$admin_user" -p "$admin_pass" -a \
-c /data/homeserver.yaml http://localhost:8008 2>&1)
if [[ "$result" == *"already taken"* ]]; then
isNotice "Matrix admin '$admin_user' already exists — leaving the existing account alone."
else
checkSuccess "Creating Matrix admin account '$admin_user'"
fi
}
matrix_install_post()
{
local app_name="$1"
local server_name
server_name=$(_matrixServerName "$app_name")
local admin_user="${CFG_MATRIX_ADMIN_USERNAME:-admin}"
echo ""
isNotice "Matrix homeserver:"
echo ""
echo " Server name : ${server_name}"
echo " Your user ID : @${admin_user}:${server_name}"
echo " Password : ${CFG_MATRIX_ADMIN_PASSWORD_1}"
echo ""
echo " Sign in through the Element web interface, or any Matrix client"
echo " (Element mobile/desktop, FluffyChat, Nheko) using the server name"
echo " above."
echo ""
echo " Registration is ${CFG_MATRIX_ENABLE_REGISTRATION:-false}. To invite"
echo " others while it stays closed, create their accounts with:"
echo " docker exec -it matrix-synapse register_new_matrix_user \\"
echo " -c /data/homeserver.yaml http://localhost:8008"
echo ""
}