feat(secrets): real VAPID keypair for mastodon, slot-numbered DB passwords

VAPID: the two values are the halves of one P-256 keypair, not independent
secrets — the browser verifies that a push is signed by the private key matching
the public key it subscribed with. The RANDOMIZED* generators mint each
placeholder on its own, so they produced two unrelated strings and web push could
never have worked. Generate the pair in mastodon_install_post_setup the way stoat
already does, encoded as Mastodon's webpush gem expects: unpadded URL-safe base64
of the 32-byte private scalar and the 65-byte uncompressed public point, sliced
out of the SEC1 DER. Verified by rebuilding the key from the emitted private half
and re-deriving the public point — openssl accepts it and the point matches.

Generated once and never rotated (rotation would invalidate every subscription),
but a pair of the wrong shape is replaced, so an install carrying the old
unrelated strings heals itself on next install — their public half is 42 chars
where a real point is 87.

Slots: CFG_<APP>_DB_PASSWORD -> CFG_<APP>_DB_PASSWORD_1 and likewise for
DB_ROOT_PASSWORD, across mastodon, owncloud, mattermost, matrix, nextcloud and
bookstack, so a database credential is always a numbered slot and a second one is
just _2. Renaming a key means reconciliation drops the old and adds the new
holding its placeholder, so an existing install regenerates unless the value is
carried over first — documented, including that the old file survives as
.<app>.config.bak.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-18 19:42:50 +01:00
parent be7d6813ef
commit 4685320353
19 changed files with 183 additions and 61 deletions

View File

@ -31,8 +31,8 @@ CFG_BOOKSTACK_ADMIN_PASSWORD=RANDOMIZEDPASSWORD3
# auto-generated, and (unlike a RANDOMIZED* placeholder in the compose) # auto-generated, and (unlike a RANDOMIZED* placeholder in the compose)
# preserved across reinstalls. DB_PASSWORD is shared by the app + db services. # preserved across reinstalls. DB_PASSWORD is shared by the app + db services.
CFG_BOOKSTACK_APP_KEY=RANDOMIZEDAPPKEY1 CFG_BOOKSTACK_APP_KEY=RANDOMIZEDAPPKEY1
CFG_BOOKSTACK_DB_PASSWORD=RANDOMIZEDPASSWORD1 CFG_BOOKSTACK_DB_PASSWORD_1=RANDOMIZEDPASSWORD1
CFG_BOOKSTACK_DB_ROOT_PASSWORD=RANDOMIZEDPASSWORD2 CFG_BOOKSTACK_DB_ROOT_PASSWORD_1=RANDOMIZEDPASSWORD2
# #
# ============================================================================= # =============================================================================
# METADATA # METADATA

View File

@ -15,7 +15,7 @@ services:
- DB_HOST=bookstack_db - DB_HOST=bookstack_db
- DB_PORT=3306 - DB_PORT=3306
- DB_USERNAME=bookstack - DB_USERNAME=bookstack
- DB_PASSWORD=BOOKSTACK_DB_PASSWORD_DATA #LIBREPORTAL|BOOKSTACK_DB_PASSWORD_TAG|BOOKSTACK_DB_PASSWORD_DATA - DB_PASSWORD=BOOKSTACK_DB_PASSWORD_1_DATA #LIBREPORTAL|BOOKSTACK_DB_PASSWORD_1_TAG|BOOKSTACK_DB_PASSWORD_1_DATA
- DB_DATABASE=bookstackapp - DB_DATABASE=bookstackapp
volumes: volumes:
- SOCKET_DATA #LIBREPORTAL|SOCKET_TAG|SOCKET_DATA - SOCKET_DATA #LIBREPORTAL|SOCKET_TAG|SOCKET_DATA
@ -60,10 +60,10 @@ services:
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA - TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
- MYSQL_ROOT_PASSWORD=BOOKSTACK_DB_ROOT_PASSWORD_DATA #LIBREPORTAL|BOOKSTACK_DB_ROOT_PASSWORD_TAG|BOOKSTACK_DB_ROOT_PASSWORD_DATA - MYSQL_ROOT_PASSWORD=BOOKSTACK_DB_ROOT_PASSWORD_1_DATA #LIBREPORTAL|BOOKSTACK_DB_ROOT_PASSWORD_1_TAG|BOOKSTACK_DB_ROOT_PASSWORD_1_DATA
- MYSQL_DATABASE=bookstackapp - MYSQL_DATABASE=bookstackapp
- MYSQL_USER=bookstack - MYSQL_USER=bookstack
- MYSQL_PASSWORD=BOOKSTACK_DB_PASSWORD_DATA #LIBREPORTAL|BOOKSTACK_DB_PASSWORD_TAG|BOOKSTACK_DB_PASSWORD_DATA - MYSQL_PASSWORD=BOOKSTACK_DB_PASSWORD_1_DATA #LIBREPORTAL|BOOKSTACK_DB_PASSWORD_1_TAG|BOOKSTACK_DB_PASSWORD_1_DATA
volumes: volumes:
- ./db:/config - ./db:/config
restart: unless-stopped restart: unless-stopped

View File

@ -3,7 +3,7 @@ networks:
external: true external: true
services: services:
mastodon-service: #LIBREPORTAL|SERVICE_TAG_1|mastodon-service mastodon-service: #LIBREPORTAL|SERVICE_TAG_3|mastodon-service
container_name: mastodon-service container_name: mastodon-service
image: tootsuite/mastodon:v4.6 #LIBREPORTAL|MASTODON_VERSION_TAG|v4.6 image: tootsuite/mastodon:v4.6 #LIBREPORTAL|MASTODON_VERSION_TAG|v4.6
ports: ports:
@ -16,7 +16,7 @@ services:
- LOCAL_DOMAIN=DOMAINSUBNAME_DATA #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA - LOCAL_DOMAIN=DOMAINSUBNAME_DATA #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA
- DB_HOST=mastodon-postgres - DB_HOST=mastodon-postgres
- DB_USER=MASTODON_DB_USER_DATA #LIBREPORTAL|MASTODON_DB_USER_TAG|MASTODON_DB_USER_DATA - DB_USER=MASTODON_DB_USER_DATA #LIBREPORTAL|MASTODON_DB_USER_TAG|MASTODON_DB_USER_DATA
- DB_PASS=MASTODON_DB_PASSWORD_DATA #LIBREPORTAL|MASTODON_DB_PASSWORD_TAG|MASTODON_DB_PASSWORD_DATA - DB_PASS=MASTODON_DB_PASSWORD_1_DATA #LIBREPORTAL|MASTODON_DB_PASSWORD_1_TAG|MASTODON_DB_PASSWORD_1_DATA
- DB_NAME=MASTODON_DB_NAME_DATA #LIBREPORTAL|MASTODON_DB_NAME_TAG|MASTODON_DB_NAME_DATA - DB_NAME=MASTODON_DB_NAME_DATA #LIBREPORTAL|MASTODON_DB_NAME_TAG|MASTODON_DB_NAME_DATA
- REDIS_HOST=mastodon-redis - REDIS_HOST=mastodon-redis
- SECRET_KEY_BASE=MASTODON_SECRET_KEY_BASE_DATA #LIBREPORTAL|MASTODON_SECRET_KEY_BASE_TAG|MASTODON_SECRET_KEY_BASE_DATA - SECRET_KEY_BASE=MASTODON_SECRET_KEY_BASE_DATA #LIBREPORTAL|MASTODON_SECRET_KEY_BASE_TAG|MASTODON_SECRET_KEY_BASE_DATA
@ -55,20 +55,20 @@ services:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_3 #LIBREPORTAL|IP_TAG_3|IP_DATA_3 ipv4_address: IP_DATA_3 #LIBREPORTAL|IP_TAG_3|IP_DATA_3
mastodon-postgres: mastodon-postgres: #LIBREPORTAL|SERVICE_TAG_1|mastodon-postgres
image: postgres:15 image: postgres:15
container_name: mastodon-postgres container_name: mastodon-postgres
environment: environment:
- POSTGRES_DB=MASTODON_DB_NAME_DATA #LIBREPORTAL|MASTODON_DB_NAME_TAG|MASTODON_DB_NAME_DATA - POSTGRES_DB=MASTODON_DB_NAME_DATA #LIBREPORTAL|MASTODON_DB_NAME_TAG|MASTODON_DB_NAME_DATA
- POSTGRES_USER=MASTODON_DB_USER_DATA #LIBREPORTAL|MASTODON_DB_USER_TAG|MASTODON_DB_USER_DATA - POSTGRES_USER=MASTODON_DB_USER_DATA #LIBREPORTAL|MASTODON_DB_USER_TAG|MASTODON_DB_USER_DATA
- POSTGRES_PASSWORD=MASTODON_DB_PASSWORD_DATA #LIBREPORTAL|MASTODON_DB_PASSWORD_TAG|MASTODON_DB_PASSWORD_DATA - POSTGRES_PASSWORD=MASTODON_DB_PASSWORD_1_DATA #LIBREPORTAL|MASTODON_DB_PASSWORD_1_TAG|MASTODON_DB_PASSWORD_1_DATA
volumes: volumes:
- ./postgres:/var/lib/postgresql/data - ./postgres:/var/lib/postgresql/data
networks: networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_1 #LIBREPORTAL|IP_TAG_1|IP_DATA_1 ipv4_address: IP_DATA_1 #LIBREPORTAL|IP_TAG_1|IP_DATA_1
mastodon-redis: mastodon-redis: #LIBREPORTAL|SERVICE_TAG_2|mastodon-redis
image: redis:7 image: redis:7
container_name: mastodon-redis container_name: mastodon-redis
command: redis-server --save 60 1 --loglevel warning command: redis-server --save 60 1 --loglevel warning

View File

@ -34,11 +34,17 @@ CFG_MASTODON_HEADSCALE=false
# every user out. # every user out.
# OTP_SECRET = protects stored two-factor enrolments. Rotating it invalidates # OTP_SECRET = protects stored two-factor enrolments. Rotating it invalidates
# them, and anyone with 2FA on needs it reset before they can log in. # them, and anyone with 2FA on needs it reset before they can log in.
# VAPID_PRIVATE_KEY / VAPID_PUBLIC_KEY = Web Push identity. # VAPID_PRIVATE_KEY / VAPID_PUBLIC_KEY = Web Push identity. These two are the
# halves of one P-256 keypair, so they cannot be generated independently the
# way the other secrets are — mastodon_install_post_setup replaces the pair
# below with a real one on first install, and never touches it again.
# RANDOMIZEDVAPID<n> is kept here only so the tag always has something
# substitutable: if key generation ever fails, Mastodon starts with push
# broken rather than with a literal placeholder as its key.
# #
CFG_MASTODON_DB_NAME=RANDOMIZEDUSERNAME1 CFG_MASTODON_DB_NAME=RANDOMIZEDUSERNAME1
CFG_MASTODON_DB_USER=RANDOMIZEDUSERNAME2 CFG_MASTODON_DB_USER=RANDOMIZEDUSERNAME2
CFG_MASTODON_DB_PASSWORD=RANDOMIZEDPASSWORD1 CFG_MASTODON_DB_PASSWORD_1=RANDOMIZEDPASSWORD1
CFG_MASTODON_SECRET_KEY_BASE=RANDOMIZEDHEX1 CFG_MASTODON_SECRET_KEY_BASE=RANDOMIZEDHEX1
CFG_MASTODON_OTP_SECRET=RANDOMIZEDHEX2 CFG_MASTODON_OTP_SECRET=RANDOMIZEDHEX2
CFG_MASTODON_VAPID_PRIVATE_KEY=RANDOMIZEDVAPID1 CFG_MASTODON_VAPID_PRIVATE_KEY=RANDOMIZEDVAPID1

View File

@ -0,0 +1,71 @@
#!/bin/bash
# Mastodon install hooks.
# Web Push identity.
# ---------------------------------------------------------------------------
# The two VAPID values are not independent secrets — they are the two halves of
# one P-256 keypair, and the browser verifies that the push request is signed by
# the private key matching the public key it subscribed with. The framework's
# RANDOMIZED* generators mint each placeholder on its own, so they can produce
# two well-formed strings but never a *pair*: what shipped before was two
# unrelated random values, and web push could not work with them. Stoat hits the
# same wall and solves it the same way, in its own install hook.
#
# Mastodon's webpush gem expects unpadded URL-safe base64 of the raw key
# material: the 32-byte private scalar (43 chars) and the 65-byte uncompressed
# public point (87 chars). Both are sliced out of the SEC1 DER, whose layout for
# prime256v1 is a fixed 121 bytes — a 2-byte SEQUENCE header, INTEGER 1, then
# `04 20` introducing the private scalar at offset 7, and the public point as
# the trailing field. Both slices were cross-checked against `openssl ec -text`.
#
# Runs at install_post_setup: the deployed config exists by then (so there is
# something to write to) and the compose has not been templated yet (so the pair
# reaches the compose on this same install).
#
# Generated once, then left alone. Rotating the pair invalidates every push
# subscription clients are holding, so an existing well-formed pair is never
# replaced — including across reinstalls.
mastodon_install_post_setup()
{
local app_name="${1:-mastodon}"
local config_file="${containers_dir}${app_name}/${app_name}.config"
[[ -f "$config_file" ]] || return 0
# Keep a pair that already has the right shape. The length test is also what
# retires the old values: two independent RANDOMIZEDVAPID strings are the
# wrong length for the public half, so an install carrying them regenerates
# once and is correct from then on.
if [[ "$CFG_MASTODON_VAPID_PRIVATE_KEY" =~ ^[A-Za-z0-9_-]{43}$ \
&& "$CFG_MASTODON_VAPID_PUBLIC_KEY" =~ ^[A-Za-z0-9_-]{87}$ ]]; then
return 0
fi
local pem der
pem=$(mktemp) || return 0
der=$(mktemp) || { rm -f "$pem"; return 0; }
if ! openssl ecparam -name prime256v1 -genkey -noout -out "$pem" 2>/dev/null \
|| ! openssl ec -in "$pem" -outform DER -out "$der" 2>/dev/null; then
rm -f "$pem" "$der"
isError "Could not generate a VAPID keypair for $app_name — web push will not work."
return 0
fi
local vapid_private vapid_public
vapid_private=$(head -c 39 "$der" | tail -c 32 | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
vapid_public=$(tail -c 65 "$der" | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
rm -f "$pem" "$der"
# Refuse to write a malformed pair over a working one — a short read or an
# openssl build with a different DER layout would otherwise quietly break
# push instead of leaving it as it was.
if [[ ! "$vapid_private" =~ ^[A-Za-z0-9_-]{43}$ || ! "$vapid_public" =~ ^[A-Za-z0-9_-]{87}$ ]]; then
isError "Generated VAPID keypair had an unexpected shape — leaving $app_name's push keys alone."
return 0
fi
updateConfigOption "CFG_MASTODON_VAPID_PRIVATE_KEY" "$vapid_private" "$config_file"
updateConfigOption "CFG_MASTODON_VAPID_PUBLIC_KEY" "$vapid_public" "$config_file"
isSuccessful "Generated a VAPID keypair for $app_name web push."
}

View File

@ -96,14 +96,14 @@ services:
# No `user:` override — the postgres entrypoint starts as root, chowns # No `user:` override — the postgres entrypoint starts as root, chowns
# PGDATA and drops privileges, which works under rootless because # PGDATA and drops privileges, which works under rootless because
# container-root is the install user that owns the mount. # container-root is the install user that owns the mount.
matrix-postgres: matrix-postgres: #LIBREPORTAL|SERVICE_TAG_3|matrix-postgres
image: postgres:15-alpine image: postgres:15-alpine
container_name: matrix-postgres container_name: matrix-postgres
restart: unless-stopped restart: unless-stopped
environment: environment:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA - TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
- POSTGRES_USER=synapse - POSTGRES_USER=synapse
- POSTGRES_PASSWORD=MATRIX_DB_PASSWORD_DATA #LIBREPORTAL|MATRIX_DB_PASSWORD_TAG|MATRIX_DB_PASSWORD_DATA - POSTGRES_PASSWORD=MATRIX_DB_PASSWORD_1_DATA #LIBREPORTAL|MATRIX_DB_PASSWORD_1_TAG|MATRIX_DB_PASSWORD_1_DATA
- POSTGRES_DB=synapse - POSTGRES_DB=synapse
# Not optional. Synapse refuses to start against a database with any # Not optional. Synapse refuses to start against a database with any
# other collation or ctype — it needs deterministic byte ordering for # other collation or ctype — it needs deterministic byte ordering for

View File

@ -39,11 +39,11 @@ CFG_MATRIX_ADMIN_USERNAME=admin
CFG_MATRIX_ADMIN_PASSWORD=RANDOMIZEDPASSWORD1 CFG_MATRIX_ADMIN_PASSWORD=RANDOMIZEDPASSWORD1
CFG_MATRIX_MONITORING=false CFG_MATRIX_MONITORING=false
# Postgres password for the `synapse` role, fed to the compose via # Postgres password for the `synapse` role, fed to the compose via
# #LIBREPORTAL|MATRIX_DB_PASSWORD_TAG| and written into homeserver.yaml by the # #LIBREPORTAL|MATRIX_DB_PASSWORD_1_TAG| and written into homeserver.yaml by the
# install hook. Generated on first install and preserved across reinstalls — # install hook. Generated on first install and preserved across reinstalls —
# initdb sets it once when the volume is created, so a regenerated value would # initdb sets it once when the volume is created, so a regenerated value would
# leave Synapse unable to open its own database. # leave Synapse unable to open its own database.
CFG_MATRIX_DB_PASSWORD=RANDOMIZEDPASSWORD2 CFG_MATRIX_DB_PASSWORD_1=RANDOMIZEDPASSWORD2
# #
# ============================================================================= # =============================================================================
# METADATA # METADATA

View File

@ -47,12 +47,12 @@ matrix_install_post_compose()
fi fi
# Must match the password the compose handed to Postgres. Read it back from # Must match the password the compose handed to Postgres. Read it back from
# the deployed compose rather than from CFG_MATRIX_DB_PASSWORD: this hook # 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 # runs after templating, so the compose is the settled value, and it stays
# correct even on an install whose config still holds the placeholder. # correct even on an install whose config still holds the placeholder.
local db_password local db_password
db_password=$(tagsManagerGetTagContent "$app_dir/docker-compose.yml" "MATRIX_DB_PASSWORD_TAG") db_password=$(tagsManagerGetTagContent "$app_dir/docker-compose.yml" "MATRIX_DB_PASSWORD_1_TAG")
if [[ -z "$db_password" || "$db_password" == "MATRIX_DB_PASSWORD_DATA" ]]; then 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." isError "Database password was not generated in the compose file — aborting Synapse configuration."
return 1 return 1
fi fi

View File

@ -34,7 +34,7 @@ services:
# Fixed role and database name, random password. The database is # Fixed role and database name, random password. The database is
# only reachable on the internal docker network, and a generated # only reachable on the internal docker network, and a generated
# username buys nothing while making manual psql recovery painful. # username buys nothing while making manual psql recovery painful.
- MM_SQLSETTINGS_DATASOURCE=postgres://mattermost:MATTERMOST_DB_PASSWORD_DATA@mattermost-postgres:5432/mattermost?sslmode=disable&connect_timeout=10 #LIBREPORTAL|MATTERMOST_DB_PASSWORD_TAG|MATTERMOST_DB_PASSWORD_DATA - MM_SQLSETTINGS_DATASOURCE=postgres://mattermost:MATTERMOST_DB_PASSWORD_1_DATA@mattermost-postgres:5432/mattermost?sslmode=disable&connect_timeout=10 #LIBREPORTAL|MATTERMOST_DB_PASSWORD_1_TAG|MATTERMOST_DB_PASSWORD_1_DATA
# Mattermost builds every absolute link (invites, password resets, # Mattermost builds every absolute link (invites, password resets,
# CORS and websocket origin checks) from this. Wrong value and the # CORS and websocket origin checks) from this. Wrong value and the
# web client connects but the websocket is rejected, which shows up # web client connects but the websocket is rejected, which shows up
@ -76,14 +76,14 @@ services:
# root, chowns PGDATA to the postgres user and then drops privileges. Under # root, chowns PGDATA to the postgres user and then drops privileges. Under
# rootless Docker container-root *is* the install user on the host, so it # rootless Docker container-root *is* the install user on the host, so it
# owns ./postgres and the chown succeeds. Pinning a uid would break that. # owns ./postgres and the chown succeeds. Pinning a uid would break that.
mattermost-postgres: mattermost-postgres: #LIBREPORTAL|SERVICE_TAG_2|mattermost-postgres
image: postgres:15-alpine image: postgres:15-alpine
container_name: mattermost-postgres container_name: mattermost-postgres
restart: unless-stopped restart: unless-stopped
environment: environment:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA - TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
- POSTGRES_USER=mattermost - POSTGRES_USER=mattermost
- POSTGRES_PASSWORD=MATTERMOST_DB_PASSWORD_DATA #LIBREPORTAL|MATTERMOST_DB_PASSWORD_TAG|MATTERMOST_DB_PASSWORD_DATA - POSTGRES_PASSWORD=MATTERMOST_DB_PASSWORD_1_DATA #LIBREPORTAL|MATTERMOST_DB_PASSWORD_1_TAG|MATTERMOST_DB_PASSWORD_1_DATA
- POSTGRES_DB=mattermost - POSTGRES_DB=mattermost
volumes: volumes:
- ./postgres:/var/lib/postgresql/data - ./postgres:/var/lib/postgresql/data

View File

@ -27,10 +27,10 @@ CFG_MATTERMOST_AUTHELIA=false
CFG_MATTERMOST_HEADSCALE=false CFG_MATTERMOST_HEADSCALE=false
CFG_MATTERMOST_MONITORING=false CFG_MATTERMOST_MONITORING=false
# Postgres password for the `mattermost` role, fed to the compose via # Postgres password for the `mattermost` role, fed to the compose via
# #LIBREPORTAL|MATTERMOST_DB_PASSWORD_TAG| (both the server's datasource URL and # #LIBREPORTAL|MATTERMOST_DB_PASSWORD_1_TAG| (both the server's datasource URL and
# the database's own env). Generated on first install and preserved across # the database's own env). Generated on first install and preserved across
# reinstalls — initdb sets it once when the volume is created. # reinstalls — initdb sets it once when the volume is created.
CFG_MATTERMOST_DB_PASSWORD=RANDOMIZEDPASSWORD1 CFG_MATTERMOST_DB_PASSWORD_1=RANDOMIZEDPASSWORD1
# #
# ============================================================================= # =============================================================================
# METADATA # METADATA

View File

@ -16,7 +16,7 @@ services:
- MYSQL_HOST=nextcloud-db - MYSQL_HOST=nextcloud-db
- MYSQL_DATABASE=NEXTCLOUD_DB_NAME_DATA #LIBREPORTAL|NEXTCLOUD_DB_NAME_TAG|NEXTCLOUD_DB_NAME_DATA - MYSQL_DATABASE=NEXTCLOUD_DB_NAME_DATA #LIBREPORTAL|NEXTCLOUD_DB_NAME_TAG|NEXTCLOUD_DB_NAME_DATA
- MYSQL_USER=NEXTCLOUD_DB_USER_DATA #LIBREPORTAL|NEXTCLOUD_DB_USER_TAG|NEXTCLOUD_DB_USER_DATA - MYSQL_USER=NEXTCLOUD_DB_USER_DATA #LIBREPORTAL|NEXTCLOUD_DB_USER_TAG|NEXTCLOUD_DB_USER_DATA
- MYSQL_PASSWORD=NEXTCLOUD_DB_PASSWORD_DATA #LIBREPORTAL|NEXTCLOUD_DB_PASSWORD_TAG|NEXTCLOUD_DB_PASSWORD_DATA - MYSQL_PASSWORD=NEXTCLOUD_DB_PASSWORD_1_DATA #LIBREPORTAL|NEXTCLOUD_DB_PASSWORD_1_TAG|NEXTCLOUD_DB_PASSWORD_1_DATA
- NEXTCLOUD_ADMIN_USER=NEXTCLOUD_ADMIN_USER_DATA #LIBREPORTAL|NEXTCLOUD_ADMIN_USER_TAG|NEXTCLOUD_ADMIN_USER_DATA - NEXTCLOUD_ADMIN_USER=NEXTCLOUD_ADMIN_USER_DATA #LIBREPORTAL|NEXTCLOUD_ADMIN_USER_TAG|NEXTCLOUD_ADMIN_USER_DATA
- NEXTCLOUD_ADMIN_PASSWORD=NEXTCLOUD_ADMIN_PASSWORD_DATA #LIBREPORTAL|NEXTCLOUD_ADMIN_PASSWORD_TAG|NEXTCLOUD_ADMIN_PASSWORD_DATA - NEXTCLOUD_ADMIN_PASSWORD=NEXTCLOUD_ADMIN_PASSWORD_DATA #LIBREPORTAL|NEXTCLOUD_ADMIN_PASSWORD_TAG|NEXTCLOUD_ADMIN_PASSWORD_DATA
- NEXTCLOUD_TRUSTED_DOMAINS=NEXTCLOUD_TRUSTED_DOMAINS_DATA #LIBREPORTAL|NEXTCLOUD_TRUSTED_DOMAINS_TAG|NEXTCLOUD_TRUSTED_DOMAINS_DATA - NEXTCLOUD_TRUSTED_DOMAINS=NEXTCLOUD_TRUSTED_DOMAINS_DATA #LIBREPORTAL|NEXTCLOUD_TRUSTED_DOMAINS_TAG|NEXTCLOUD_TRUSTED_DOMAINS_DATA
@ -49,10 +49,10 @@ services:
restart: unless-stopped restart: unless-stopped
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
environment: environment:
- MARIADB_ROOT_PASSWORD=NEXTCLOUD_DB_ROOT_PASSWORD_DATA #LIBREPORTAL|NEXTCLOUD_DB_ROOT_PASSWORD_TAG|NEXTCLOUD_DB_ROOT_PASSWORD_DATA - MARIADB_ROOT_PASSWORD=NEXTCLOUD_DB_ROOT_PASSWORD_1_DATA #LIBREPORTAL|NEXTCLOUD_DB_ROOT_PASSWORD_1_TAG|NEXTCLOUD_DB_ROOT_PASSWORD_1_DATA
- MARIADB_DATABASE=NEXTCLOUD_DB_NAME_DATA #LIBREPORTAL|NEXTCLOUD_DB_NAME_TAG|NEXTCLOUD_DB_NAME_DATA - MARIADB_DATABASE=NEXTCLOUD_DB_NAME_DATA #LIBREPORTAL|NEXTCLOUD_DB_NAME_TAG|NEXTCLOUD_DB_NAME_DATA
- MARIADB_USER=NEXTCLOUD_DB_USER_DATA #LIBREPORTAL|NEXTCLOUD_DB_USER_TAG|NEXTCLOUD_DB_USER_DATA - MARIADB_USER=NEXTCLOUD_DB_USER_DATA #LIBREPORTAL|NEXTCLOUD_DB_USER_TAG|NEXTCLOUD_DB_USER_DATA
- MARIADB_PASSWORD=NEXTCLOUD_DB_PASSWORD_DATA #LIBREPORTAL|NEXTCLOUD_DB_PASSWORD_TAG|NEXTCLOUD_DB_PASSWORD_DATA - MARIADB_PASSWORD=NEXTCLOUD_DB_PASSWORD_1_DATA #LIBREPORTAL|NEXTCLOUD_DB_PASSWORD_1_TAG|NEXTCLOUD_DB_PASSWORD_1_DATA
volumes: volumes:
- ./db_data:/var/lib/mysql - ./db_data:/var/lib/mysql
networks: networks:

View File

@ -27,8 +27,8 @@ CFG_NEXTCLOUD_ADMIN_USER=admin
CFG_NEXTCLOUD_ADMIN_PASSWORD=RANDOMIZEDPASSWORD1 CFG_NEXTCLOUD_ADMIN_PASSWORD=RANDOMIZEDPASSWORD1
CFG_NEXTCLOUD_DB_NAME=nextcloud CFG_NEXTCLOUD_DB_NAME=nextcloud
CFG_NEXTCLOUD_DB_USER=nextcloud CFG_NEXTCLOUD_DB_USER=nextcloud
CFG_NEXTCLOUD_DB_PASSWORD=RANDOMIZEDPASSWORD2 CFG_NEXTCLOUD_DB_PASSWORD_1=RANDOMIZEDPASSWORD2
CFG_NEXTCLOUD_DB_ROOT_PASSWORD=RANDOMIZEDPASSWORD3 CFG_NEXTCLOUD_DB_ROOT_PASSWORD_1=RANDOMIZEDPASSWORD3
CFG_NEXTCLOUD_AUTH_PROFILE=multi_user CFG_NEXTCLOUD_AUTH_PROFILE=multi_user
# #
# ============================================================================= # =============================================================================

View File

@ -22,7 +22,7 @@ services:
- OWNCLOUD_DB_TYPE=mysql - OWNCLOUD_DB_TYPE=mysql
- OWNCLOUD_DB_NAME=OWNCLOUD_DB_NAME_DATA #LIBREPORTAL|OWNCLOUD_DB_NAME_TAG|OWNCLOUD_DB_NAME_DATA - OWNCLOUD_DB_NAME=OWNCLOUD_DB_NAME_DATA #LIBREPORTAL|OWNCLOUD_DB_NAME_TAG|OWNCLOUD_DB_NAME_DATA
- OWNCLOUD_DB_USERNAME=OWNCLOUD_DB_USER_DATA #LIBREPORTAL|OWNCLOUD_DB_USER_TAG|OWNCLOUD_DB_USER_DATA - OWNCLOUD_DB_USERNAME=OWNCLOUD_DB_USER_DATA #LIBREPORTAL|OWNCLOUD_DB_USER_TAG|OWNCLOUD_DB_USER_DATA
- OWNCLOUD_DB_PASSWORD=OWNCLOUD_DB_PASSWORD_DATA #LIBREPORTAL|OWNCLOUD_DB_PASSWORD_TAG|OWNCLOUD_DB_PASSWORD_DATA - OWNCLOUD_DB_PASSWORD=OWNCLOUD_DB_PASSWORD_1_DATA #LIBREPORTAL|OWNCLOUD_DB_PASSWORD_1_TAG|OWNCLOUD_DB_PASSWORD_1_DATA
- OWNCLOUD_DB_HOST=owncloud-mariadb - OWNCLOUD_DB_HOST=owncloud-mariadb
- OWNCLOUD_ADMIN_USERNAME=OWNCLOUD_ADMIN_USERNAME_DATA #LIBREPORTAL|OWNCLOUD_ADMIN_USERNAME_TAG|OWNCLOUD_ADMIN_USERNAME_DATA - OWNCLOUD_ADMIN_USERNAME=OWNCLOUD_ADMIN_USERNAME_DATA #LIBREPORTAL|OWNCLOUD_ADMIN_USERNAME_TAG|OWNCLOUD_ADMIN_USERNAME_DATA
- OWNCLOUD_ADMIN_PASSWORD=OWNCLOUD_ADMIN_PASSWORD_DATA #LIBREPORTAL|OWNCLOUD_ADMIN_PASSWORD_TAG|OWNCLOUD_ADMIN_PASSWORD_DATA - OWNCLOUD_ADMIN_PASSWORD=OWNCLOUD_ADMIN_PASSWORD_DATA #LIBREPORTAL|OWNCLOUD_ADMIN_PASSWORD_TAG|OWNCLOUD_ADMIN_PASSWORD_DATA
@ -73,13 +73,13 @@ services:
restart: unless-stopped restart: unless-stopped
hostname: mariadb hostname: mariadb
environment: environment:
- MYSQL_ROOT_PASSWORD=OWNCLOUD_DB_ROOT_PASSWORD_DATA #LIBREPORTAL|OWNCLOUD_DB_ROOT_PASSWORD_TAG|OWNCLOUD_DB_ROOT_PASSWORD_DATA - MYSQL_ROOT_PASSWORD=OWNCLOUD_DB_ROOT_PASSWORD_1_DATA #LIBREPORTAL|OWNCLOUD_DB_ROOT_PASSWORD_1_TAG|OWNCLOUD_DB_ROOT_PASSWORD_1_DATA
- MYSQL_USER=OWNCLOUD_DB_USER_DATA #LIBREPORTAL|OWNCLOUD_DB_USER_TAG|OWNCLOUD_DB_USER_DATA - MYSQL_USER=OWNCLOUD_DB_USER_DATA #LIBREPORTAL|OWNCLOUD_DB_USER_TAG|OWNCLOUD_DB_USER_DATA
- MYSQL_PASSWORD=OWNCLOUD_DB_PASSWORD_DATA #LIBREPORTAL|OWNCLOUD_DB_PASSWORD_TAG|OWNCLOUD_DB_PASSWORD_DATA - MYSQL_PASSWORD=OWNCLOUD_DB_PASSWORD_1_DATA #LIBREPORTAL|OWNCLOUD_DB_PASSWORD_1_TAG|OWNCLOUD_DB_PASSWORD_1_DATA
- MYSQL_DATABASE=OWNCLOUD_DB_NAME_DATA #LIBREPORTAL|OWNCLOUD_DB_NAME_TAG|OWNCLOUD_DB_NAME_DATA - MYSQL_DATABASE=OWNCLOUD_DB_NAME_DATA #LIBREPORTAL|OWNCLOUD_DB_NAME_TAG|OWNCLOUD_DB_NAME_DATA
command: ["--max-allowed-packet=128M", "--innodb-log-file-size=64M"] command: ["--max-allowed-packet=128M", "--innodb-log-file-size=64M"]
healthcheck: healthcheck:
test: ["CMD", "mysqladmin", "ping", "-u", "root", "--password=OWNCLOUD_DB_ROOT_PASSWORD_DATA"] #LIBREPORTAL|OWNCLOUD_DB_ROOT_PASSWORD_TAG|OWNCLOUD_DB_ROOT_PASSWORD_DATA test: ["CMD", "mysqladmin", "ping", "-u", "root", "--password=OWNCLOUD_DB_ROOT_PASSWORD_1_DATA"] #LIBREPORTAL|OWNCLOUD_DB_ROOT_PASSWORD_1_TAG|OWNCLOUD_DB_ROOT_PASSWORD_1_DATA
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5

View File

@ -37,8 +37,8 @@ CFG_OWNCLOUD_ADMIN_USERNAME=RANDOMIZEDUSERNAME1
CFG_OWNCLOUD_ADMIN_PASSWORD=RANDOMIZEDPASSWORD1 CFG_OWNCLOUD_ADMIN_PASSWORD=RANDOMIZEDPASSWORD1
CFG_OWNCLOUD_DB_NAME=RANDOMIZEDUSERNAME2 CFG_OWNCLOUD_DB_NAME=RANDOMIZEDUSERNAME2
CFG_OWNCLOUD_DB_USER=RANDOMIZEDUSERNAME3 CFG_OWNCLOUD_DB_USER=RANDOMIZEDUSERNAME3
CFG_OWNCLOUD_DB_PASSWORD=RANDOMIZEDPASSWORD2 CFG_OWNCLOUD_DB_PASSWORD_1=RANDOMIZEDPASSWORD2
CFG_OWNCLOUD_DB_ROOT_PASSWORD=RANDOMIZEDPASSWORD3 CFG_OWNCLOUD_DB_ROOT_PASSWORD_1=RANDOMIZEDPASSWORD3
# #
# ============================================================================= # =============================================================================
# METADATA # METADATA

View File

@ -78,7 +78,7 @@ services:
# No `user:` override: the entrypoint starts as root, chowns /data/db and # No `user:` override: the entrypoint starts as root, chowns /data/db and
# then drops to the mongodb user. Under rootless Docker container-root is # then drops to the mongodb user. Under rootless Docker container-root is
# the install user on the host, which owns the bind mount, so that works. # the install user on the host, which owns the bind mount, so that works.
rocketchat-db: rocketchat-db: #LIBREPORTAL|SERVICE_TAG_2|rocketchat-db
image: mongo:8.0 #LIBREPORTAL|ROCKETCHAT_MONGO_VERSION_TAG|8.0 image: mongo:8.0 #LIBREPORTAL|ROCKETCHAT_MONGO_VERSION_TAG|8.0
container_name: rocketchat-db container_name: rocketchat-db
restart: unless-stopped restart: unless-stopped

View File

@ -10,6 +10,14 @@
# service names in Revolt.toml — keep resolving, and LibrePortal still gets the # service names in Revolt.toml — keep resolving, and LibrePortal still gets the
# prefixed container names its port, firewall and backup layers key on. # prefixed container names its port, firewall and backup layers key on.
# #
# SERVICE_TAG_n carries the compose *key*, not the container name, for two
# reasons: ipUpdateComposeTags allocates exactly one IP per SERVICE_TAG and fills
# IP_TAG_i only where SERVICE_TAG_i exists (so every service with an
# ipv4_address needs one, index-aligned, or it deploys with a literal
# IP_DATA_n and compose refuses to start), and `libreportal app restart <app>
# <service>` passes the value straight to `docker compose restart`, which only
# understands keys.
#
# Upstream fronts the whole stack with Caddy doing path routing (/api, /ws, # Upstream fronts the whole stack with Caddy doing path routing (/api, /ws,
# /autumn, ...). That is kept as-is and Traefik simply proxies to it, which is # /autumn, ...). That is kept as-is and Traefik simply proxies to it, which is
# upstream's own supported "behind another reverse proxy" mode — reimplementing # upstream's own supported "behind another reverse proxy" mode — reimplementing
@ -27,7 +35,7 @@ services:
# Upstream's healthcheck is kept rather than the LibrePortal HEALTHCHECK_TAG: # Upstream's healthcheck is kept rather than the LibrePortal HEALTHCHECK_TAG:
# half the stack uses `depends_on: condition: service_healthy` against it, so # half the stack uses `depends_on: condition: service_healthy` against it, so
# disabling the healthcheck would deadlock the boot order. Same for rabbit. # disabling the healthcheck would deadlock the boot order. Same for rabbit.
database: database: #LIBREPORTAL|SERVICE_TAG_1|database
container_name: stoat-database container_name: stoat-database
image: mongo:8.0 #LIBREPORTAL|STOAT_MONGO_VERSION_TAG|8.0 image: mongo:8.0 #LIBREPORTAL|STOAT_MONGO_VERSION_TAG|8.0
restart: unless-stopped restart: unless-stopped
@ -46,7 +54,7 @@ services:
ipv4_address: IP_DATA_1 #LIBREPORTAL|IP_TAG_1|IP_DATA_1 ipv4_address: IP_DATA_1 #LIBREPORTAL|IP_TAG_1|IP_DATA_1
# Valkey — event message broker and KV store. # Valkey — event message broker and KV store.
redis: redis: #LIBREPORTAL|SERVICE_TAG_2|redis
container_name: stoat-redis container_name: stoat-redis
image: valkey/valkey:9-alpine #LIBREPORTAL|STOAT_VALKEY_VERSION_TAG|9-alpine image: valkey/valkey:9-alpine #LIBREPORTAL|STOAT_VALKEY_VERSION_TAG|9-alpine
restart: unless-stopped restart: unless-stopped
@ -57,7 +65,7 @@ services:
ipv4_address: IP_DATA_2 #LIBREPORTAL|IP_TAG_2|IP_DATA_2 ipv4_address: IP_DATA_2 #LIBREPORTAL|IP_TAG_2|IP_DATA_2
# RabbitMQ — internal message broker (push notifications, voice events). # RabbitMQ — internal message broker (push notifications, voice events).
rabbit: rabbit: #LIBREPORTAL|SERVICE_TAG_3|rabbit
container_name: stoat-rabbit container_name: stoat-rabbit
image: rabbitmq:4-alpine #LIBREPORTAL|STOAT_RABBITMQ_VERSION_TAG|4-alpine image: rabbitmq:4-alpine #LIBREPORTAL|STOAT_RABBITMQ_VERSION_TAG|4-alpine
restart: unless-stopped restart: unless-stopped
@ -82,7 +90,7 @@ services:
# The bucket-name aliases are load-bearing: the file server addresses # The bucket-name aliases are load-bearing: the file server addresses
# buckets virtual-host style (<bucket>.minio), so without these the DNS # buckets virtual-host style (<bucket>.minio), so without these the DNS
# lookup fails and every upload errors. # lookup fails and every upload errors.
minio: minio: #LIBREPORTAL|SERVICE_TAG_4|minio
container_name: stoat-minio container_name: stoat-minio
image: minio/minio:latest #LIBREPORTAL|STOAT_MINIO_VERSION_TAG|latest image: minio/minio:latest #LIBREPORTAL|STOAT_MINIO_VERSION_TAG|latest
restart: unless-stopped restart: unless-stopped
@ -111,7 +119,7 @@ services:
# One-shot: creates the uploads bucket, then exits. Not a failure when you # One-shot: creates the uploads bucket, then exits. Not a failure when you
# see it stopped. # see it stopped.
createbuckets: createbuckets: #LIBREPORTAL|SERVICE_TAG_5|createbuckets
container_name: stoat-createbuckets container_name: stoat-createbuckets
image: minio/mc:latest #LIBREPORTAL|STOAT_MINIO_MC_VERSION_TAG|latest image: minio/mc:latest #LIBREPORTAL|STOAT_MINIO_MC_VERSION_TAG|latest
depends_on: depends_on:
@ -139,7 +147,7 @@ services:
# Caddy — internal path router for the whole stack. This is the only service # Caddy — internal path router for the whole stack. This is the only service
# Traefik talks to, and the only one carrying a Traefik router. # Traefik talks to, and the only one carrying a Traefik router.
caddy: #LIBREPORTAL|SERVICE_TAG_1|caddy caddy: #LIBREPORTAL|SERVICE_TAG_6|caddy
container_name: stoat-caddy container_name: stoat-caddy
image: caddy:2-alpine #LIBREPORTAL|STOAT_CADDY_VERSION_TAG|2-alpine image: caddy:2-alpine #LIBREPORTAL|STOAT_CADDY_VERSION_TAG|2-alpine
restart: unless-stopped restart: unless-stopped
@ -184,7 +192,7 @@ services:
# GLUETUN_ON_END # GLUETUN_ON_END
# API server. # API server.
api: api: #LIBREPORTAL|SERVICE_TAG_7|api
container_name: stoat-api container_name: stoat-api
image: ghcr.io/stoatchat/api:v0.15.1 #LIBREPORTAL|STOAT_VERSION_TAG|v0.15.1 image: ghcr.io/stoatchat/api:v0.15.1 #LIBREPORTAL|STOAT_VERSION_TAG|v0.15.1
restart: unless-stopped restart: unless-stopped
@ -205,7 +213,7 @@ services:
- api - api
# Websocket / events service. # Websocket / events service.
events: events: #LIBREPORTAL|SERVICE_TAG_8|events
container_name: stoat-events container_name: stoat-events
image: ghcr.io/stoatchat/events:v0.15.1 #LIBREPORTAL|STOAT_EVENTS_VERSION_TAG|v0.15.1 image: ghcr.io/stoatchat/events:v0.15.1 #LIBREPORTAL|STOAT_EVENTS_VERSION_TAG|v0.15.1
restart: unless-stopped restart: unless-stopped
@ -224,7 +232,7 @@ services:
- events - events
# Autumn — file server. # Autumn — file server.
autumn: autumn: #LIBREPORTAL|SERVICE_TAG_9|autumn
container_name: stoat-autumn container_name: stoat-autumn
image: ghcr.io/stoatchat/file-server:v0.15.1 #LIBREPORTAL|STOAT_AUTUMN_VERSION_TAG|v0.15.1 image: ghcr.io/stoatchat/file-server:v0.15.1 #LIBREPORTAL|STOAT_AUTUMN_VERSION_TAG|v0.15.1
restart: unless-stopped restart: unless-stopped
@ -243,7 +251,7 @@ services:
- autumn - autumn
# January — link metadata and image proxy. # January — link metadata and image proxy.
january: january: #LIBREPORTAL|SERVICE_TAG_10|january
container_name: stoat-january container_name: stoat-january
image: ghcr.io/stoatchat/proxy:v0.15.1 #LIBREPORTAL|STOAT_JANUARY_VERSION_TAG|v0.15.1 image: ghcr.io/stoatchat/proxy:v0.15.1 #LIBREPORTAL|STOAT_JANUARY_VERSION_TAG|v0.15.1
restart: unless-stopped restart: unless-stopped
@ -258,7 +266,7 @@ services:
# Gifbox — Tenor proxy for the GIF picker. Inert until a Tenor API key is # Gifbox — Tenor proxy for the GIF picker. Inert until a Tenor API key is
# added to secrets.env; see the upstream Guides.md. # added to secrets.env; see the upstream Guides.md.
gifbox: gifbox: #LIBREPORTAL|SERVICE_TAG_11|gifbox
container_name: stoat-gifbox container_name: stoat-gifbox
image: ghcr.io/stoatchat/gifbox:v0.15.1 #LIBREPORTAL|STOAT_GIFBOX_VERSION_TAG|v0.15.1 image: ghcr.io/stoatchat/gifbox:v0.15.1 #LIBREPORTAL|STOAT_GIFBOX_VERSION_TAG|v0.15.1
restart: unless-stopped restart: unless-stopped
@ -272,7 +280,7 @@ services:
- gifbox - gifbox
# Scheduled task daemon. # Scheduled task daemon.
crond: crond: #LIBREPORTAL|SERVICE_TAG_12|crond
container_name: stoat-crond container_name: stoat-crond
image: ghcr.io/stoatchat/crond:v0.15.1 #LIBREPORTAL|STOAT_CROND_VERSION_TAG|v0.15.1 image: ghcr.io/stoatchat/crond:v0.15.1 #LIBREPORTAL|STOAT_CROND_VERSION_TAG|v0.15.1
restart: unless-stopped restart: unless-stopped
@ -289,7 +297,7 @@ services:
ipv4_address: IP_DATA_12 #LIBREPORTAL|IP_TAG_12|IP_DATA_12 ipv4_address: IP_DATA_12 #LIBREPORTAL|IP_TAG_12|IP_DATA_12
# Push notification daemon. # Push notification daemon.
pushd: pushd: #LIBREPORTAL|SERVICE_TAG_13|pushd
container_name: stoat-pushd container_name: stoat-pushd
image: ghcr.io/stoatchat/pushd:v0.15.1 #LIBREPORTAL|STOAT_PUSHD_VERSION_TAG|v0.15.1 image: ghcr.io/stoatchat/pushd:v0.15.1 #LIBREPORTAL|STOAT_PUSHD_VERSION_TAG|v0.15.1
restart: unless-stopped restart: unless-stopped
@ -308,7 +316,7 @@ services:
ipv4_address: IP_DATA_13 #LIBREPORTAL|IP_TAG_13|IP_DATA_13 ipv4_address: IP_DATA_13 #LIBREPORTAL|IP_TAG_13|IP_DATA_13
# Voice ingress daemon — receives LiveKit's webhooks. # Voice ingress daemon — receives LiveKit's webhooks.
voice-ingress: voice-ingress: #LIBREPORTAL|SERVICE_TAG_14|voice-ingress
container_name: stoat-voice-ingress container_name: stoat-voice-ingress
image: ghcr.io/stoatchat/voice-ingress:v0.15.1 #LIBREPORTAL|STOAT_VOICE_INGRESS_VERSION_TAG|v0.15.1 image: ghcr.io/stoatchat/voice-ingress:v0.15.1 #LIBREPORTAL|STOAT_VOICE_INGRESS_VERSION_TAG|v0.15.1
restart: unless-stopped restart: unless-stopped
@ -337,7 +345,7 @@ services:
# to work from outside the LAN: # to work from outside the LAN:
# sudo ufw allow 50000:50100/udp # sudo ufw allow 50000:50100/udp
# Voice still falls back to TCP 7881 without it, at the cost of latency. # Voice still falls back to TCP 7881 without it, at the cost of latency.
livekit: livekit: #LIBREPORTAL|SERVICE_TAG_15|livekit
container_name: stoat-livekit container_name: stoat-livekit
image: ghcr.io/stoatchat/livekit-server:v1.9.13 #LIBREPORTAL|STOAT_LIVEKIT_VERSION_TAG|v1.9.13 image: ghcr.io/stoatchat/livekit-server:v1.9.13 #LIBREPORTAL|STOAT_LIVEKIT_VERSION_TAG|v1.9.13
restart: unless-stopped restart: unless-stopped
@ -357,7 +365,7 @@ services:
- livekit - livekit
# The web client itself. Served by Caddy at /. # The web client itself. Served by Caddy at /.
web: web: #LIBREPORTAL|SERVICE_TAG_16|web
container_name: stoat-web container_name: stoat-web
image: ghcr.io/stoatchat/for-web:0c31cf0 #LIBREPORTAL|STOAT_WEB_VERSION_TAG|0c31cf0 image: ghcr.io/stoatchat/for-web:0c31cf0 #LIBREPORTAL|STOAT_WEB_VERSION_TAG|0c31cf0
restart: unless-stopped restart: unless-stopped

View File

@ -156,8 +156,42 @@ initialised with. That is the same desync the old mechanism caused on every
reinstall; the difference is that it now happens at most once, because the values reinstall; the difference is that it now happens at most once, because the values
are preserved from then on. are preserved from then on.
If you have one of these installed and want to avoid it, capture the credentials ### Database password keys are now numbered slots
**before** reinstalling. With the app stopped:
`CFG_<APP>_DB_PASSWORD` became `CFG_<APP>_DB_PASSWORD_1`, and
`CFG_<APP>_DB_ROOT_PASSWORD` became `CFG_<APP>_DB_ROOT_PASSWORD_1`, so a database
credential is always a numbered slot and an app that needs a second one just adds
`_2`. This also touches **nextcloud** and **bookstack**, which were otherwise
unaffected by the change above.
A rename is a delete plus an add as far as config reconciliation is concerned: the
old key is dropped and the new one arrives holding its `RANDOMIZED*` placeholder,
so the next install generates a fresh password rather than keeping the current
one. To carry the existing password over, copy it into the new key before
reinstalling — reconciliation leaves the previous file as a hidden
`.<app>.config.bak` sibling, so the old value is still there:
```bash
grep DB_PASSWORD <containers-dir>/<app>/.<app>.config.bak
```
Paste each value into the matching `_1` key in `<containers-dir>/<app>/<app>.config`.
### Mastodon web push
`VAPID_PRIVATE_KEY` and `VAPID_PUBLIC_KEY` were generated as two independent
random strings, which is not a keypair — web push could never have worked. They
are now generated together as a P-256 keypair by the install hook, in the format
Mastodon's webpush gem expects. Any install still carrying the old values
regenerates the pair once on its next install (the old public half is the wrong
length to be a valid point, which is what triggers it) and is correct from then
on. Existing push subscriptions were not working anyway, so nothing is lost;
clients re-subscribe on next login.
### Carrying credentials across, generally
If you have one of these installed and want to avoid regeneration, capture the
credentials **before** reinstalling. With the app stopped:
```bash ```bash
grep -E 'POSTGRES_|MYSQL_|SECRET_KEY_BASE|OTP_SECRET|VAPID_|RABBITMQ_DEFAULT_PASS' <containers-dir>/<app>/docker-compose.yml grep -E 'POSTGRES_|MYSQL_|SECRET_KEY_BASE|OTP_SECRET|VAPID_|RABBITMQ_DEFAULT_PASS' <containers-dir>/<app>/docker-compose.yml

View File

@ -645,6 +645,7 @@ declare -gA LP_FN_MAP=(
[manifestReadFromSnapshot]="backup/manifest/manifest_read.sh" [manifestReadFromSnapshot]="backup/manifest/manifest_read.sh"
[manifestRemove]="backup/manifest/manifest_write.sh" [manifestRemove]="backup/manifest/manifest_write.sh"
[manifestWrite]="backup/manifest/manifest_write.sh" [manifestWrite]="backup/manifest/manifest_write.sh"
[mastodon_install_post_setup]="mastodon/scripts/mastodon_install_hooks.sh"
[mastodon_upgrade_verify]="mastodon/scripts/mastodon_upgrade_hooks.sh" [mastodon_upgrade_verify]="mastodon/scripts/mastodon_upgrade_hooks.sh"
[matrix_install_post]="matrix/scripts/matrix_install_hooks.sh" [matrix_install_post]="matrix/scripts/matrix_install_hooks.sh"
[matrix_install_post_compose]="matrix/scripts/matrix_install_hooks.sh" [matrix_install_post_compose]="matrix/scripts/matrix_install_hooks.sh"
@ -1700,6 +1701,7 @@ declare -gA LP_FN_ROOT=(
[manifestReadFromSnapshot]="scripts" [manifestReadFromSnapshot]="scripts"
[manifestRemove]="scripts" [manifestRemove]="scripts"
[manifestWrite]="scripts" [manifestWrite]="scripts"
[mastodon_install_post_setup]="containers"
[mastodon_upgrade_verify]="containers" [mastodon_upgrade_verify]="containers"
[matrix_install_post]="containers" [matrix_install_post]="containers"
[matrix_install_post_compose]="containers" [matrix_install_post_compose]="containers"
@ -2789,6 +2791,7 @@ manifestReadField() { unset -f manifestReadField; __lpAutoload "${install_script
manifestReadFromSnapshot() { unset -f manifestReadFromSnapshot; __lpAutoload "${install_scripts_dir}backup/manifest/manifest_read.sh"; manifestReadFromSnapshot "$@"; } manifestReadFromSnapshot() { unset -f manifestReadFromSnapshot; __lpAutoload "${install_scripts_dir}backup/manifest/manifest_read.sh"; manifestReadFromSnapshot "$@"; }
manifestRemove() { unset -f manifestRemove; __lpAutoload "${install_scripts_dir}backup/manifest/manifest_write.sh"; manifestRemove "$@"; } manifestRemove() { unset -f manifestRemove; __lpAutoload "${install_scripts_dir}backup/manifest/manifest_write.sh"; manifestRemove "$@"; }
manifestWrite() { unset -f manifestWrite; __lpAutoload "${install_scripts_dir}backup/manifest/manifest_write.sh"; manifestWrite "$@"; } manifestWrite() { unset -f manifestWrite; __lpAutoload "${install_scripts_dir}backup/manifest/manifest_write.sh"; manifestWrite "$@"; }
mastodon_install_post_setup() { unset -f mastodon_install_post_setup; __lpAutoload "${install_containers_dir}mastodon/scripts/mastodon_install_hooks.sh"; mastodon_install_post_setup "$@"; }
mastodon_upgrade_verify() { unset -f mastodon_upgrade_verify; __lpAutoload "${install_containers_dir}mastodon/scripts/mastodon_upgrade_hooks.sh"; mastodon_upgrade_verify "$@"; } mastodon_upgrade_verify() { unset -f mastodon_upgrade_verify; __lpAutoload "${install_containers_dir}mastodon/scripts/mastodon_upgrade_hooks.sh"; mastodon_upgrade_verify "$@"; }
matrix_install_post() { unset -f matrix_install_post; __lpAutoload "${install_containers_dir}matrix/scripts/matrix_install_hooks.sh"; matrix_install_post "$@"; } matrix_install_post() { unset -f matrix_install_post; __lpAutoload "${install_containers_dir}matrix/scripts/matrix_install_hooks.sh"; matrix_install_post "$@"; }
matrix_install_post_compose() { unset -f matrix_install_post_compose; __lpAutoload "${install_containers_dir}matrix/scripts/matrix_install_hooks.sh"; matrix_install_post_compose "$@"; } matrix_install_post_compose() { unset -f matrix_install_post_compose; __lpAutoload "${install_containers_dir}matrix/scripts/matrix_install_hooks.sh"; matrix_install_post_compose "$@"; }

View File

@ -343,14 +343,14 @@ PORTEOF
"advanced": true, "advanced": true,
"default": "nextcloud" "default": "nextcloud"
}, },
"NEXTCLOUD_DB_PASSWORD": { "NEXTCLOUD_DB_PASSWORD_1": {
"category": "advanced", "category": "advanced",
"label": "Database Password", "label": "Database Password",
"type": "password", "type": "password",
"tooltip": "MariaDB password for the Nextcloud user (auto-generated; persists across reinstalls)", "tooltip": "MariaDB password for the Nextcloud user (auto-generated; persists across reinstalls)",
"advanced": true "advanced": true
}, },
"NEXTCLOUD_DB_ROOT_PASSWORD": { "NEXTCLOUD_DB_ROOT_PASSWORD_1": {
"category": "advanced", "category": "advanced",
"label": "Database Root Password", "label": "Database Root Password",
"type": "password", "type": "password",
@ -371,7 +371,7 @@ PORTEOF
"tooltip": "Postgres role Mastodon connects with (internal to the docker network). Set by initdb when the volume is created.", "tooltip": "Postgres role Mastodon connects with (internal to the docker network). Set by initdb when the volume is created.",
"advanced": true "advanced": true
}, },
"MASTODON_DB_PASSWORD": { "MASTODON_DB_PASSWORD_1": {
"category": "advanced", "category": "advanced",
"label": "Database Password", "label": "Database Password",
"type": "password", "type": "password",
@ -420,28 +420,28 @@ PORTEOF
"tooltip": "MariaDB account ownCloud connects with (internal to the docker network)", "tooltip": "MariaDB account ownCloud connects with (internal to the docker network)",
"advanced": true "advanced": true
}, },
"OWNCLOUD_DB_PASSWORD": { "OWNCLOUD_DB_PASSWORD_1": {
"category": "advanced", "category": "advanced",
"label": "Database Password", "label": "Database Password",
"type": "password", "type": "password",
"tooltip": "MariaDB password for the ownCloud user (auto-generated; persists across reinstalls)", "tooltip": "MariaDB password for the ownCloud user (auto-generated; persists across reinstalls)",
"advanced": true "advanced": true
}, },
"OWNCLOUD_DB_ROOT_PASSWORD": { "OWNCLOUD_DB_ROOT_PASSWORD_1": {
"category": "advanced", "category": "advanced",
"label": "Database Root Password", "label": "Database Root Password",
"type": "password", "type": "password",
"tooltip": "MariaDB root password (auto-generated; kept separate from the app user password so root can be left alone if you rotate the app account)", "tooltip": "MariaDB root password (auto-generated; kept separate from the app user password so root can be left alone if you rotate the app account)",
"advanced": true "advanced": true
}, },
"MATTERMOST_DB_PASSWORD": { "MATTERMOST_DB_PASSWORD_1": {
"category": "advanced", "category": "advanced",
"label": "Database Password", "label": "Database Password",
"type": "password", "type": "password",
"tooltip": "Postgres password for the mattermost role (auto-generated; persists across reinstalls)", "tooltip": "Postgres password for the mattermost role (auto-generated; persists across reinstalls)",
"advanced": true "advanced": true
}, },
"MATRIX_DB_PASSWORD": { "MATRIX_DB_PASSWORD_1": {
"category": "advanced", "category": "advanced",
"label": "Database Password", "label": "Database Password",
"type": "password", "type": "password",