Both hooks read their host (and matrix its database password) back out of the deployed docker-compose.yml. That cannot work: install_post_compose runs after the compose TEMPLATE is copied but before dockerConfigSetupFileWithData fills the tags, so at that point the file still holds raw placeholders. Matrix aborted with "Database password was not generated in the compose file" even though the password had been generated correctly — it just was not in the compose yet. Derive the host from port_subdomains[0] + domain_full instead, both already in scope from variables_init_app, applying the same empty/@/root rule as tagsProcessorPortSubdomains so the computed name and the Traefik rule generated later cannot drift apart. Matrix takes its database password from CFG_MATRIX_DB_PASSWORD_1, which is where the secret is generated and remembered and is the same variable the compose tag is filled from a step later. The error messages now name the actual missing thing — the domain — rather than blaming the compose file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
239 lines
10 KiB
Bash
239 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 — the value that becomes server_name.
|
|
#
|
|
# Built from the port arrays and $domain_full that variables_init_app has
|
|
# already put in scope, NOT by reading DOMAINSUBNAME_TAG_1 out of the deployed
|
|
# compose: install_post_compose runs before dockerConfigSetupFileWithData, so at
|
|
# that point the compose still holds raw placeholders.
|
|
#
|
|
# port_subdomains[0] is CFG_MATRIX_PORT_1, the Synapse router — deliberately not
|
|
# $host_setup, which for this two-host app resolves to Element instead. The
|
|
# empty/@/root cases mirror tagsProcessorPortSubdomains so the name computed
|
|
# here and the Traefik rule generated later cannot drift apart.
|
|
_matrixServerName()
|
|
{
|
|
local sub="${port_subdomains[0]}"
|
|
[[ -z "$domain_full" ]] && return 1
|
|
if [[ "$sub" == "@" || "$sub" == "root" ]]; then
|
|
echo "$domain_full"
|
|
elif [[ -n "$sub" ]]; then
|
|
echo "${sub}.${domain_full}"
|
|
else
|
|
echo "matrix.${domain_full}"
|
|
fi
|
|
}
|
|
|
|
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)
|
|
if [[ -z "$server_name" ]]; then
|
|
isError "No domain is configured — cannot determine the Matrix server_name."
|
|
isNotice "Set CFG_DOMAIN_${CFG_MATRIX_DOMAIN:-1} (General → Network), then reinstall."
|
|
return 1
|
|
fi
|
|
|
|
# Same value the compose will hand to Postgres. Taken from the config rather
|
|
# than the compose because the compose is not substituted yet at this point;
|
|
# the config is where the secret is generated and remembered, and the
|
|
# #LIBREPORTAL|MATRIX_DB_PASSWORD_1_TAG| line is filled from this very
|
|
# variable a step later, so the two cannot disagree.
|
|
local db_password="$CFG_MATRIX_DB_PASSWORD_1"
|
|
if [[ -z "$db_password" || "$db_password" == RANDOMIZEDPASSWORD* ]]; then
|
|
isError "CFG_MATRIX_DB_PASSWORD_1 was not generated — 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)
|
|
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 ""
|
|
}
|