Compare commits

...

5 Commits

Author SHA1 Message Date
librelad
9919eea138 stoat: add the ex-Revolt stack as the closest Discord equivalent
Sixteen containers: MongoDB, Valkey, RabbitMQ, MinIO and eleven Stoat services.
Servers, channels, roles and voice/video through LiveKit — the nearest thing in
the catalogue to Discord itself, at the price of being much the heaviest app in
it. Does not federate.

The compose service keys are deliberately kept identical to upstream's
(database, redis, api, autumn, ...) while container_name is prefixed stoat-.
Compose registers both on the network, so upstream's internal defaults keep
resolving and LibrePortal still gets the prefixed names its port, firewall and
backup layers key on.

Upstream's Caddy is kept as the internal path router and Traefik simply proxies
to it, which is upstream's own supported behind-a-reverse-proxy mode —
reimplementing eight path routes as Traefik labels would be a second copy to
keep in sync for nothing. The install hook is a non-interactive port of
generate_config.sh, and it never rewrites an existing secrets.env:
REVOLT__FILES__ENCRYPTION_KEY decrypts every file ever uploaded, so
regenerating it would orphan the whole media store.

LiveKit's UDP media range is published literally rather than through the port
table, because the firewall rebuild emits /tcp rules only and a range declared
there would produce a wrong rule rather than no rule. Voice falls back to TCP
7881 until the range is opened by hand; the post-install notice says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 05:28:46 +01:00
librelad
50059ea1b8 rocketchat: add Rocket.Chat with a single-node Mongo replica set
Rocket.Chat tails the Mongo oplog for realtime delivery, and a standalone
mongod has no oplog — so the database has to be a replica set even with one
member.

Uses the official mongo image rather than bitnami/mongodb (which upstream's own
compose uses) because Bitnami moved its catalog behind a paid registry and the
free tags are no longer dependable for a long-lived install. The cost is that
rs.initiate() is not automatic, so the post-start hook runs it once — guarded by
rs.status() so a reinstall over restored data doesn't re-initiate a live set,
and followed by a wait for the member to report itself primary.

Mongo runs without auth: enabling it on a replica set also requires a shared
keyfile for member-to-member auth, which is a lot of moving parts for a database
that is never published outside the docker network.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 05:28:46 +01:00
librelad
c0025d8211 mattermost: add Team Edition as a low-friction chat app
One container against Postgres, with the polished desktop and mobile clients
that make it the least demanding of the four chat options.

Runs as the bind-mount owner via USER_TAG: the image bakes in USER mattermost
(uid 2000) so it never runs as root and cannot chown its own data directory,
which under rootless Docker means it dies on first write.

CFG_MATTERMOST_AUTHELIA stays false — OIDC/SAML is a paid tier here, so
forward-auth would block the native clients from the API without buying single
sign-on in exchange.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 05:28:31 +01:00
librelad
b9e334dc49 matrix: add Synapse + Element as a federated chat app
Synapse on Postgres plus the Element web client, on two subdomains: the
homeserver on matrix.<domain> (which becomes server_name, so IDs read
@alice:matrix.<domain>) and Element on element.<domain>.

Two hosts rather than one because server_name then matches the host Traefik
already terminates TLS for, so 'serve_server_wellknown: true' is all the
federation delegation needed and nothing has to be published at the apex
domain — which this app has no way to configure.

CFG_MATRIX_AUTHELIA is pinned false and documented: forward-auth in front of
/_matrix locks out every client and every federating peer, since they carry
Matrix access tokens and cannot follow a redirect. Real SSO goes through the
OIDC block in resources/homeserver.yaml instead.

The install hook generates the signing key once via upstream's own 'generate'
command and refuses to regenerate it over an existing install — a new key would
be rejected by every server that had cached the old one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 05:28:31 +01:00
librelad
9084280ea8 backup: add a mongo driver for live, consistent dumps
Rocket.Chat and Stoat are both MongoDB-backed, and the backup engine only
understood postgres, mysql/mariadb and sqlite — so a live snapshot of either
would have captured a torn data directory that may not even mount.

Adds mongo as a fourth kind: mongodump --archive on the backup side,
mongorestore --archive --drop on the restore side (idempotent, so the caller's
retry loop works the same as it does for pg_dump --clean), and a ping-based
readiness probe that also waits out a replica set electing its primary.

Credentials are optional. The shared sh preamble sets them from
MONGO_INITDB_ROOT_USERNAME/PASSWORD when present and passes nothing when not,
built with 'set --' so a password containing spaces survives word splitting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 05:28:20 +01:00
21 changed files with 1831 additions and 3 deletions

View File

@ -0,0 +1,116 @@
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
external: true
services:
# Synapse — the homeserver. Everything that matters lives in
# ./data/homeserver.yaml, written by matrix_install_post_compose from
# resources/homeserver.yaml; Synapse takes no meaningful configuration from
# the environment, so there is little to see here.
matrix-synapse: #LIBREPORTAL|SERVICE_TAG_1|matrix-synapse
container_name: matrix-synapse
image: matrixdotorg/synapse:v1.158.0 #LIBREPORTAL|MATRIX_VERSION_TAG|v1.158.0
# Synapse writes the media store, and under rootless Docker the image's
# own uid maps to a host sub-UID that owns nothing. Same fix as the
# other apps: run as whoever owns the bind mounts.
user: "USER_DATA" #LIBREPORTAL|USER_TAG|USER_DATA
restart: unless-stopped
# GLUETUN_OFF_BEGIN
ports:
- "PORTS_DATA_1" #LIBREPORTAL|PORTS_TAG_1|PORTS_DATA_1
# GLUETUN_OFF_END
volumes:
- ./data:/data
environment:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
- SYNAPSE_CONFIG_PATH=/data/homeserver.yaml
depends_on:
- matrix-postgres
labels:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
libreportal.backup.db: "postgres:matrix-postgres:postgres:"
# The media store holds every uploaded file and avatar. It is not in
# the database, so without this it would not come back on restore.
libreportal.backup.files: "matrix-synapse:/data/media_store:data/media_store"
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.matrix-synapse.entrypoints: web,websecure
traefik.http.routers.matrix-synapse.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1
traefik.http.routers.matrix-synapse.tls: true
traefik.http.routers.matrix-synapse.tls.certresolver: production
traefik.http.services.matrix-synapse.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.matrix-synapse.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
healthcheck:
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA
# GLUETUN_OFF_BEGIN
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_1 #LIBREPORTAL|IP_TAG_1|IP_DATA_1
# GLUETUN_OFF_END
# GLUETUN_ON_BEGIN
# network_mode: "container:gluetun-service"
# GLUETUN_ON_END
# Element web — a static single-page app served by nginx. It talks to
# Synapse from the user's browser, not server-side, so it needs no link to
# the homeserver container beyond the base_url baked into config.json.
matrix-element: #LIBREPORTAL|SERVICE_TAG_2|matrix-element
container_name: matrix-element
image: vectorim/element-web:v1.12.25 #LIBREPORTAL|MATRIX_ELEMENT_VERSION_TAG|v1.12.25
restart: unless-stopped
# GLUETUN_OFF_BEGIN
ports:
- "PORTS_DATA_2" #LIBREPORTAL|PORTS_TAG_2|PORTS_DATA_2
# GLUETUN_OFF_END
volumes:
- ./element/config.json:/app/config.json:ro
environment:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
labels:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_2_BEGIN
traefik.http.routers.matrix-element.entrypoints: web,websecure
traefik.http.routers.matrix-element.rule: Host(`DOMAINSUBNAME_DATA_2`) #LIBREPORTAL|DOMAINSUBNAME_TAG_2|DOMAINSUBNAME_DATA_2
traefik.http.routers.matrix-element.tls: true
traefik.http.routers.matrix-element.tls.certresolver: production
traefik.http.services.matrix-element.loadbalancer.server.port: PORT_INTERNAL_DATA_2 #LIBREPORTAL|PORT_INTERNAL_TAG_2|PORT_INTERNAL_DATA_2
traefik.http.routers.matrix-element.middlewares: MIDDLEWARE_DATA_2 #LIBREPORTAL|MIDDLEWARE_TAG_2|MIDDLEWARE_DATA_2
# TRAEFIK_PORT_2_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
healthcheck:
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA
# GLUETUN_OFF_BEGIN
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_2 #LIBREPORTAL|IP_TAG_2|IP_DATA_2
# GLUETUN_OFF_END
# GLUETUN_ON_BEGIN
# network_mode: "container:gluetun-service"
# GLUETUN_ON_END
# No `user:` override — the postgres entrypoint starts as root, chowns
# PGDATA and drops privileges, which works under rootless because
# container-root is the install user that owns the mount.
matrix-postgres:
image: postgres:15-alpine
container_name: matrix-postgres
restart: unless-stopped
environment:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
- POSTGRES_USER=synapse
- POSTGRES_PASSWORD=PASSWORD_DATA_1 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1
- POSTGRES_DB=synapse
# Not optional. Synapse refuses to start against a database with any
# other collation or ctype — it needs deterministic byte ordering for
# its indexes, and a C.UTF-8 locale is the only thing that gives it.
- POSTGRES_INITDB_ARGS=--encoding=UTF8 --locale=C
volumes:
- ./postgres:/var/lib/postgresql/data
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_3 #LIBREPORTAL|IP_TAG_3|IP_DATA_3

View File

@ -0,0 +1,95 @@
#
# =============================================================================
# GENERAL CONFIGURATION
# =============================================================================
# APP_NAME = name of application for use in scripts
# REQUIRES = comma-separated install prerequisites (see scripts/checks/requirements/check_app_install.sh)
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
# BACKUP = if true, include this application in backup operations
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
# ENABLE_REGISTRATION = if true, anyone who can reach the homeserver can create an account on it
# ADMIN_USERNAME = localpart of the first admin account created at install (the full ID becomes @<name>:<server_name>)
# ADMIN_PASSWORD = password for that first admin account
# MONITORING = if true, export this app's metrics to Prometheus + Grafana (needs both apps installed)
#
CFG_MATRIX_APP_NAME=matrix
# A domain and Traefik are hard requirements, not conveniences. Synapse bakes
# server_name into every event and user ID it has ever signed, and it CANNOT be
# changed afterwards without throwing the database away — so the homeserver must
# know its real public name at install time, over real TLS.
CFG_MATRIX_REQUIRES="domain,traefik"
CFG_MATRIX_BACKUP=true
CFG_MATRIX_BACKUP_STRATEGY=auto
CFG_MATRIX_UPDATE_TYPE=auto
CFG_MATRIX_COMPOSE_FILE=default
CFG_MATRIX_HEALTHCHECK=true
# Must stay false. Authelia's forward-auth would sit in front of /_matrix, which
# is the API every Matrix client and every federating server speaks — they
# authenticate with Matrix access tokens and cannot follow an Authelia redirect,
# so turning this on breaks all clients and federation at once. Synapse can do
# real SSO against Authelia instead, via the OIDC block in
# resources/homeserver.yaml.
CFG_MATRIX_AUTHELIA=false
CFG_MATRIX_HEADSCALE=false
CFG_MATRIX_ENABLE_REGISTRATION=false
CFG_MATRIX_ADMIN_USERNAME=admin
CFG_MATRIX_ADMIN_PASSWORD=RANDOMIZEDPASSWORD1
CFG_MATRIX_MONITORING=false
#
# =============================================================================
# METADATA
# =============================================================================
# CATEGORY = application category for grouping
# TITLE = display name for the application
# DESCRIPTION = short description of the application
# LONG_DESCRIPTION = detailed description of the application
# URL = source repository or documentation URL
# ACTIONS = available actions for this application
# REQUIRES_SERVICE = name of another LibrePortal app that must be installed before this one can be configured
#
CFG_MATRIX_CATEGORY="communication"
CFG_MATRIX_TITLE="Matrix"
CFG_MATRIX_DESCRIPTION="Federated Chat"
CFG_MATRIX_LONG_DESCRIPTION="Matrix is the open federated chat protocol — Spaces and rooms cover what Discord servers and channels do, with end-to-end encryption and bridges to Discord, IRC and Slack. This installs the Synapse homeserver on Postgres plus the Element web client, on their own subdomains. Because it federates, accounts on this server can talk to every other Matrix server without either side giving up control"
CFG_MATRIX_URL="https://github.com/element-hq/synapse"
CFG_MATRIX_ACTIONS="configure|install|restart|shutdown|uninstall"
CFG_MATRIX_REQUIRES_SERVICE=traefik
#
# =============================================================================
# NETWORK CONFIGURATION
# =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
#
CFG_MATRIX_DOMAIN=1
CFG_MATRIX_WHITELIST=false
CFG_MATRIX_NETWORK=default
#
# =============================================================================
# PORT CONFIGURATION
# =============================================================================
# PORT_ = port configuration: app|name|external:internal|access|protocol|login|traefik|webui|description
# - app: application name
# - name: service identifier (webui, dns, ssh, etc.)
# - external:internal: port mapping (external can be 'random' for auto-allocation)
# - access: 'public' (internet accessible), 'private' (local network only), 'disabled' (not running)
# - protocol: 'tcp' or 'udp'
# - login: if true, this port requires basic-auth via Traefik (only meaningful when traefik=true)
# - traefik: if true, Traefik handles this port (reverse proxy)
# - webui: if true, this port serves the main web interface
# - description: human-readable description of the service
#
# Two hosts, on purpose. Port 1 is the homeserver API on matrix.<domain>, which
# becomes server_name — so user IDs read @alice:matrix.<domain>. Port 2 is the
# Element web client on element.<domain>. Keeping them apart means Synapse can
# answer /.well-known/matrix/server for itself and federation needs no
# delegation from the apex domain, which this app has no way to configure.
#
# Port 1 must NOT be marked login=true: /_matrix is the client and federation
# API and basic-auth in front of it locks out every client and every peer.
#
CFG_MATRIX_PORT_1="matrix-synapse|homeserver|random:8008|public|tcp|false|true|false|Matrix Homeserver (client + federation API)||matrix"
CFG_MATRIX_PORT_2="matrix-element|webui|random:80|public|tcp|false|true|true|Element Web Interface||element"

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256" role="img" aria-label="Matrix"><rect width="256" height="256" rx="28" fill="#0dbd8b" /><path fill="#fff" d="M62 40h30v10h-8a6 6 0 0 0-6 6v144a6 6 0 0 0 6 6h8v10H62a10 10 0 0 1-10-10V50a10 10 0 0 1 10-10m132 0a10 10 0 0 1 10 10v156a10 10 0 0 1-10 10h-30v-10h8a6 6 0 0 0 6-6V56a6 6 0 0 0-6-6h-8V40z" /><path fill="#fff" d="M104 96h14v11h.4c4-8.4 12-12.6 20.6-12.6 9 0 16.6 3.8 20.6 12.6 4.8-8 13.4-12.6 22.4-12.6h.2v14.6c-1.4-.2-3-.4-4.4-.4-11.6 0-17.6 6.6-17.6 19.4V160h-15v-38.8c0-8.6-2.6-13.4-10-13.4-8.6 0-13.6 6-13.6 18.6V160h-15z" /></svg>

After

Width:  |  Height:  |  Size: 631 B

View File

@ -0,0 +1,24 @@
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://ELEMENT_HOMESERVER_PLACEHOLDER",
"server_name": "ELEMENT_HOMESERVER_PLACEHOLDER"
}
},
"brand": "Element",
"disable_custom_urls": false,
"disable_guests": true,
"disable_login_language_selector": false,
"disable_3pid_login": false,
"default_country_code": "GB",
"show_labs_settings": true,
"room_directory": {
"servers": ["ELEMENT_HOMESERVER_PLACEHOLDER", "matrix.org"]
},
"setting_defaults": {
"breadcrumbs": true
},
"jitsi": {
"preferredDomain": "meet.element.io"
}
}

View File

@ -0,0 +1,103 @@
# Synapse homeserver configuration.
#
# Copied to <app dir>/data/homeserver.yaml by matrix_install_post_compose, which
# substitutes the *_PLACEHOLDER values below. Edit the deployed copy, not this
# template — this one is only read at install time.
#
# Restart the container after editing: docker restart matrix-synapse
# server_name is permanent. It is signed into every event this server has ever
# sent and forms the second half of every user ID (@alice:<server_name>).
# Changing it later does not migrate anything — it orphans the whole database.
server_name: "SYNAPSE_SERVER_NAME_PLACEHOLDER"
public_baseurl: "https://SYNAPSE_SERVER_NAME_PLACEHOLDER/"
pid_file: /data/homeserver.pid
# Serve /.well-known/matrix/server ourselves, advertising port 443. Federation
# otherwise defaults to port 8448 on server_name, which Traefik is not
# listening on. Because server_name is the same host Traefik already terminates
# TLS for, this is all the delegation that is needed — nothing has to be
# published at the apex domain.
serve_server_wellknown: true
listeners:
# Port 8008 is the container-internal port and is deliberately hardcoded: it
# is what CFG_MATRIX_PORT_1 declares as the internal half of its mapping, and
# what the Traefik service label points at. Change one and you must change all
# three.
- port: 8008
tls: false
type: http
# Traefik terminates TLS and proxies onward, so the source address Synapse
# sees is Traefik's. Without this, rate limiting and the audit log would
# attribute every request in the world to a single internal IP.
x_forwarded: true
bind_addresses: ['0.0.0.0']
resources:
- names: [client, federation]
compress: false
database:
name: psycopg2
args:
user: synapse
password: "SYNAPSE_DB_PASSWORD_PLACEHOLDER"
dbname: synapse
host: matrix-postgres
port: 5432
cp_min: 5
cp_max: 10
log_config: "/data/log.config"
media_store_path: /data/media_store
signing_key_path: "/data/signing.key"
# Uploads. Raise max_upload_size if your users share video; remember the
# reverse proxy has its own limit too.
max_upload_size: 50M
# Open registration is off by default: a reachable homeserver with registration
# enabled will be found and used for spam within days. The install creates one
# admin account for you; invite everyone else, or turn this on deliberately via
# CFG_MATRIX_ENABLE_REGISTRATION and re-run the install.
enable_registration: SYNAPSE_ENABLE_REGISTRATION_PLACEHOLDER
enable_registration_without_verification: SYNAPSE_ENABLE_REGISTRATION_PLACEHOLDER
registration_shared_secret: "SYNAPSE_REGISTRATION_SECRET_PLACEHOLDER"
macaroon_secret_key: "SYNAPSE_MACAROON_SECRET_PLACEHOLDER"
form_secret: "SYNAPSE_FORM_SECRET_PLACEHOLDER"
report_stats: false
suppress_key_server_warning: true
# Which servers to fetch other servers' signing keys from. matrix.org is the
# conventional default; federation still works if it is unreachable, just more
# slowly on first contact with a new server.
trusted_key_servers:
- server_name: "matrix.org"
# ---------------------------------------------------------------------------
# Single sign-on against Authelia (optional)
# ---------------------------------------------------------------------------
# CFG_MATRIX_AUTHELIA must stay false — that switch puts Authelia's forward-auth
# in front of /_matrix, which breaks every client. Real SSO is done here
# instead, with Synapse as an OIDC client of Authelia.
#
# Register the client in Authelia's configuration.yml first, then uncomment and
# fill in the block below and restart the container.
#
# oidc_providers:
# - idp_id: authelia
# idp_name: "Authelia"
# issuer: "https://auth.<your domain>"
# client_id: "synapse"
# client_secret: "<the secret you set in Authelia>"
# scopes: ["openid", "profile", "email"]
# user_mapping_provider:
# config:
# localpart_template: "{{ user.preferred_username }}"
# display_name_template: "{{ user.name }}"
# email_template: "{{ user.email }}"
#
# The redirect URI to register in Authelia is:
# https://SYNAPSE_SERVER_NAME_PLACEHOLDER/_synapse/client/oidc/callback

View File

@ -0,0 +1,32 @@
# Synapse logging configuration.
#
# Logs go to stdout only, so `docker logs matrix-synapse` and the LibrePortal
# log viewer both see them, and nothing accumulates inside the container that
# the host does not rotate.
version: 1
formatters:
precise:
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
handlers:
console:
class: logging.StreamHandler
formatter: precise
loggers:
synapse.storage.SQL:
# Set to INFO to log every database query — useful when chasing a slow
# server, far too noisy for normal running.
level: WARNING
# Very chatty at INFO, and rarely what you are looking for.
synapse.access.http.8008:
level: WARNING
root:
level: INFO
handlers: [console]
disable_existing_loggers: false

View File

@ -0,0 +1,223 @@
#!/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. Reading it back
# from the deployed compose is the only way to stay in step: the value is
# generated per-install by the password tag processor.
local db_password
db_password=$(tagsManagerGetTagContent "$app_dir/docker-compose.yml" "PASSWORD_TAG_1")
if [[ -z "$db_password" || "$db_password" == "PASSWORD_DATA_1" ]]; 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}"
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}"
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 ""
}

View File

@ -0,0 +1,92 @@
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
external: true
services:
# Mattermost Team Edition — one app container against Postgres. The server
# and the web client are the same binary, so unlike the Matrix stack there
# is no separate frontend service to route.
mattermost-service: #LIBREPORTAL|SERVICE_TAG_1|mattermost-service
container_name: mattermost-service
image: mattermost/mattermost-team-edition:11.9 #LIBREPORTAL|MATTERMOST_VERSION_TAG|11.9
# The image bakes in `USER mattermost` (uid 2000), so it never runs as
# root and cannot chown its own bind mounts on first boot. Same problem
# vikunja has: under rootless Docker uid 2000 maps to a host sub-UID
# that owns nothing, and the container dies on its first write to
# /mattermost/data. USER_TAG resolves to the identity that actually owns
# the mounts — 0:0 under rootless, the real uid:gid under rooted.
user: "USER_DATA" #LIBREPORTAL|USER_TAG|USER_DATA
restart: unless-stopped
# GLUETUN_OFF_BEGIN
ports:
- "PORTS_DATA_1" #LIBREPORTAL|PORTS_TAG_1|PORTS_DATA_1
# GLUETUN_OFF_END
volumes:
- ./config:/mattermost/config
- ./data:/mattermost/data
- ./logs:/mattermost/logs
- ./plugins:/mattermost/plugins
- ./client-plugins:/mattermost/client/plugins
- ./bleve-indexes:/mattermost/bleve-indexes
environment:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
- MM_SQLSETTINGS_DRIVERNAME=postgres
# Fixed role and database name, random password. The database is
# only reachable on the internal docker network, and a generated
# username buys nothing while making manual psql recovery painful.
- MM_SQLSETTINGS_DATASOURCE=postgres://mattermost:PASSWORD_DATA_1@mattermost-postgres:5432/mattermost?sslmode=disable&connect_timeout=10 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1
# Mattermost builds every absolute link (invites, password resets,
# CORS and websocket origin checks) from this. Wrong value and the
# web client connects but the websocket is rejected, which shows up
# as a chat that loads and then never receives a message.
- MM_SERVICESETTINGS_SITEURL=APP_URL_DATA #LIBREPORTAL|APP_URL_TAG|APP_URL_DATA
- MM_BLEVESETTINGS_INDEXDIR=/mattermost/bleve-indexes
- MM_FILESETTINGS_DIRECTORY=/mattermost/data/
- MM_LOGSETTINGS_ENABLEFILE=true
- MM_LOGSETTINGS_FILELOCATION=/mattermost/logs
depends_on:
- mattermost-postgres
labels:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
libreportal.backup.db: "postgres:mattermost-postgres:postgres:"
libreportal.backup.files: "mattermost-service:/mattermost/data:data"
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.mattermost-service.entrypoints: web,websecure
traefik.http.routers.mattermost-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1
traefik.http.routers.mattermost-service.tls: true
traefik.http.routers.mattermost-service.tls.certresolver: production
traefik.http.services.mattermost-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.mattermost-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
healthcheck:
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA
# GLUETUN_OFF_BEGIN
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_1 #LIBREPORTAL|IP_TAG_1|IP_DATA_1
# GLUETUN_OFF_END
# GLUETUN_ON_BEGIN
# network_mode: "container:gluetun-service"
# GLUETUN_ON_END
# No `user:` override here on purpose: the postgres entrypoint starts as
# 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
# owns ./postgres and the chown succeeds. Pinning a uid would break that.
mattermost-postgres:
image: postgres:15-alpine
container_name: mattermost-postgres
restart: unless-stopped
environment:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
- POSTGRES_USER=mattermost
- POSTGRES_PASSWORD=PASSWORD_DATA_1 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1
- POSTGRES_DB=mattermost
volumes:
- ./postgres:/var/lib/postgresql/data
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_2 #LIBREPORTAL|IP_TAG_2|IP_DATA_2

View File

@ -0,0 +1,71 @@
#
# =============================================================================
# GENERAL CONFIGURATION
# =============================================================================
# APP_NAME = name of application for use in scripts
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
# BACKUP = if true, include this application in backup operations
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
# MONITORING = if true, export this app's metrics to Prometheus + Grafana (needs both apps installed)
#
CFG_MATTERMOST_APP_NAME=mattermost
CFG_MATTERMOST_BACKUP=true
CFG_MATTERMOST_BACKUP_STRATEGY=auto
CFG_MATTERMOST_UPDATE_TYPE=auto
CFG_MATTERMOST_COMPOSE_FILE=default
CFG_MATTERMOST_HEALTHCHECK=true
# Left false deliberately. Mattermost ships its own account system and its
# native desktop/mobile clients authenticate against it directly — putting
# Authelia's forward-auth in front of the web port would lock those clients out
# of the API without giving single sign-on in return (OIDC/SAML is a paid tier
# in the Team Edition shipped here). Turn it on only if you intend to use the
# web client exclusively.
CFG_MATTERMOST_AUTHELIA=false
CFG_MATTERMOST_HEADSCALE=false
CFG_MATTERMOST_MONITORING=false
#
# =============================================================================
# METADATA
# =============================================================================
# CATEGORY = application category for grouping
# TITLE = display name for the application
# DESCRIPTION = short description of the application
# LONG_DESCRIPTION = detailed description of the application
# URL = source repository or documentation URL
# ACTIONS = available actions for this application
#
CFG_MATTERMOST_CATEGORY="communication"
CFG_MATTERMOST_TITLE="Mattermost"
CFG_MATTERMOST_DESCRIPTION="Team Chat"
CFG_MATTERMOST_LONG_DESCRIPTION="Mattermost is a self-hosted team chat platform with channels, threads, file sharing and search, plus polished desktop and mobile clients. The Team Edition here is free and unlimited on users, and runs as one container against Postgres — the low-friction option if you want a Discord or Slack replacement that works the moment it starts"
CFG_MATTERMOST_URL="https://github.com/mattermost/mattermost"
CFG_MATTERMOST_ACTIONS="configure|install|restart|shutdown|uninstall"
#
# =============================================================================
# NETWORK CONFIGURATION
# =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
#
CFG_MATTERMOST_DOMAIN=1
CFG_MATTERMOST_WHITELIST=false
CFG_MATTERMOST_NETWORK=default
#
# =============================================================================
# PORT CONFIGURATION
# =============================================================================
# PORT_ = port configuration: app|name|external:internal|access|protocol|login|traefik|webui|description
# - app: application name
# - name: service identifier (webui, dns, ssh, etc.)
# - external:internal: port mapping (external can be 'random' for auto-allocation)
# - access: 'public' (internet accessible), 'private' (local network only), 'disabled' (not running)
# - protocol: 'tcp' or 'udp'
# - login: if true, this port requires basic-auth via Traefik (only meaningful when traefik=true)
# - traefik: if true, Traefik handles this port (reverse proxy)
# - webui: if true, this port serves the main web interface
# - description: human-readable description of the service
#
CFG_MATTERMOST_PORT_1="mattermost-service|webui|random:8065|public|tcp|false|true|true|Web Interface||mattermost"

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256" role="img" aria-label="Mattermost"><path fill="#1e325c" d="M128 0a128 128 0 1 0 0 256 128 128 0 0 0 0-256" /><path fill="#fff" d="M170.4 47.3a2 2 0 0 0-3.3 1.4l-.9 15.7a58.9 58.9 0 0 1 22.5 42.4c1.9 33.3-22.8 59.9-56.2 61.8s-61.4-21.6-63.3-54.9a58.9 58.9 0 0 1 18.4-45.1l1.7-15.7a2 2 0 0 0-3.1-1.8A81.2 81.2 0 0 0 48.6 118c2.6 45 40.4 78.2 85.5 75.6s76.9-40.3 74.3-85.3a81.2 81.2 0 0 0-38-60.9" /><path fill="#fff" d="m121.6 137.7 25.5-1.5c8.6-.5 13.7-9.9 9.3-17.3l-13-21.9-13.7-23a5.6 5.6 0 0 0-10.4 2.5l-1.4 26.6-1.5 25.5a9.6 9.6 0 0 0 5.2 9.1" /></svg>

After

Width:  |  Height:  |  Size: 643 B

View File

@ -0,0 +1,93 @@
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
external: true
services:
rocketchat-service: #LIBREPORTAL|SERVICE_TAG_1|rocketchat-service
container_name: rocketchat-service
image: rocketchat/rocket.chat:8.7.0 #LIBREPORTAL|ROCKETCHAT_VERSION_TAG|8.7.0
# The image runs as uid 65533 and writes uploads into /app/uploads, so
# it hits the same rootless bind-mount ownership problem as vikunja and
# mattermost. USER_TAG resolves to whoever actually owns the mounts.
user: "USER_DATA" #LIBREPORTAL|USER_TAG|USER_DATA
# Rocket.Chat exits — rather than waits — when Mongo has no elected
# primary, and on a fresh install the replica set is only initiated by
# the post-start hook. `unless-stopped` is what carries it through those
# first few seconds of crash-looping.
restart: unless-stopped
# GLUETUN_OFF_BEGIN
ports:
- "PORTS_DATA_1" #LIBREPORTAL|PORTS_TAG_1|PORTS_DATA_1
# GLUETUN_OFF_END
volumes:
- ./uploads:/app/uploads
environment:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
- PORT=PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
# ROOT_URL is what Rocket.Chat puts in invite links, OAuth redirect
# URLs and the websocket origin check. Get it wrong and the client
# loads but never connects.
- ROOT_URL=APP_URL_DATA #LIBREPORTAL|APP_URL_TAG|APP_URL_DATA
# ?replicaSet=rs0 is mandatory, not decorative: Rocket.Chat tails
# the Mongo oplog for realtime updates, and the oplog only exists on
# a replica set. Single-node is fine — it just has to be a set.
- MONGO_URL=mongodb://rocketchat-db:27017/rocketchat?replicaSet=rs0
- MONGO_OPLOG_URL=mongodb://rocketchat-db:27017/local?replicaSet=rs0
- DEPLOY_METHOD=docker
- OVERWRITE_SETTING_Show_Setup_Wizard=pending
depends_on:
- rocketchat-db
labels:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
libreportal.backup.db: "mongo:rocketchat-db:mongo_data:"
libreportal.backup.files: "rocketchat-service:/app/uploads:uploads"
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.rocketchat-service.entrypoints: web,websecure
traefik.http.routers.rocketchat-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1
traefik.http.routers.rocketchat-service.tls: true
traefik.http.routers.rocketchat-service.tls.certresolver: production
traefik.http.services.rocketchat-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.rocketchat-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
healthcheck:
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA
# GLUETUN_OFF_BEGIN
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_1 #LIBREPORTAL|IP_TAG_1|IP_DATA_1
# GLUETUN_OFF_END
# GLUETUN_ON_BEGIN
# network_mode: "container:gluetun-service"
# GLUETUN_ON_END
# Official mongo image rather than bitnami/mongodb (which upstream's own
# compose uses): Bitnami moved its catalog behind a paid registry in 2025
# and the free tags are no longer dependable for a long-lived install. The
# cost is that the replica set is not auto-initiated, so
# rocketchat_install_post_start runs rs.initiate() once, by hand.
#
# No authentication: enabling it on a replica set additionally requires a
# shared keyfile for member-to-member auth, which is a lot of moving parts
# for a database that is never published outside the docker network. The
# backup driver's mongo path works either way — it adds credential flags
# only when MONGO_INITDB_ROOT_USERNAME is set.
#
# No `user:` override: the entrypoint starts as root, chowns /data/db and
# 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.
rocketchat-db:
image: mongo:8.0 #LIBREPORTAL|ROCKETCHAT_MONGO_VERSION_TAG|8.0
container_name: rocketchat-db
restart: unless-stopped
command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
volumes:
- ./mongo_data:/data/db
- ./mongo_config:/data/configdb
environment:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_2 #LIBREPORTAL|IP_TAG_2|IP_DATA_2

View File

@ -0,0 +1,70 @@
#
# =============================================================================
# GENERAL CONFIGURATION
# =============================================================================
# APP_NAME = name of application for use in scripts
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
# BACKUP = if true, include this application in backup operations
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
# MONITORING = if true, export this app's metrics to Prometheus + Grafana (needs both apps installed)
#
CFG_ROCKETCHAT_APP_NAME=rocketchat
CFG_ROCKETCHAT_BACKUP=true
CFG_ROCKETCHAT_BACKUP_STRATEGY=auto
# Manual, not auto. Rocket.Chat runs schema migrations on boot and refuses to
# start if the image is more than one major behind the database — an unattended
# jump across majors can leave the instance down until someone intervenes.
CFG_ROCKETCHAT_UPDATE_TYPE=manual
CFG_ROCKETCHAT_COMPOSE_FILE=default
CFG_ROCKETCHAT_HEALTHCHECK=true
# Rocket.Chat's own accounts back its mobile and desktop clients; forward-auth
# in front of the web port would block those clients from the REST API.
CFG_ROCKETCHAT_AUTHELIA=false
CFG_ROCKETCHAT_HEADSCALE=false
CFG_ROCKETCHAT_MONITORING=false
#
# =============================================================================
# METADATA
# =============================================================================
# CATEGORY = application category for grouping
# TITLE = display name for the application
# DESCRIPTION = short description of the application
# LONG_DESCRIPTION = detailed description of the application
# URL = source repository or documentation URL
# ACTIONS = available actions for this application
#
CFG_ROCKETCHAT_CATEGORY="communication"
CFG_ROCKETCHAT_TITLE="Rocket.Chat"
CFG_ROCKETCHAT_DESCRIPTION="Team Chat & Video"
CFG_ROCKETCHAT_LONG_DESCRIPTION="Rocket.Chat is a mature self-hosted chat platform with channels, threads, voice and video, and native mobile and desktop clients. It integrates directly with a Jitsi Meet server for calls, so it pairs with the Jitsi Meet app already in this catalog. Note the free Community edition caps the number of active users — check the current limit before rolling it out to a large group"
CFG_ROCKETCHAT_URL="https://github.com/RocketChat/Rocket.Chat"
CFG_ROCKETCHAT_ACTIONS="configure|install|restart|shutdown|uninstall"
#
# =============================================================================
# NETWORK CONFIGURATION
# =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
#
CFG_ROCKETCHAT_DOMAIN=1
CFG_ROCKETCHAT_WHITELIST=false
CFG_ROCKETCHAT_NETWORK=default
#
# =============================================================================
# PORT CONFIGURATION
# =============================================================================
# PORT_ = port configuration: app|name|external:internal|access|protocol|login|traefik|webui|description
# - app: application name
# - name: service identifier (webui, dns, ssh, etc.)
# - external:internal: port mapping (external can be 'random' for auto-allocation)
# - access: 'public' (internet accessible), 'private' (local network only), 'disabled' (not running)
# - protocol: 'tcp' or 'udp'
# - login: if true, this port requires basic-auth via Traefik (only meaningful when traefik=true)
# - traefik: if true, Traefik handles this port (reverse proxy)
# - webui: if true, this port serves the main web interface
# - description: human-readable description of the service
#
CFG_ROCKETCHAT_PORT_1="rocketchat-service|webui|random:3000|public|tcp|false|true|true|Web Interface||rocketchat"

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256" role="img" aria-label="Rocket.Chat"><path fill="#f5455c" d="M128 40c-46 0-84 25.2-95.9 59.1-6.6-6.4-16.6-13.4-30.2-17.2-1.4-.4-2.6 1.1-1.9 2.4 5.1 9.1 13.4 26.5 12.6 43.7.8 17.2-7.5 34.6-12.6 43.7-.7 1.3.5 2.8 1.9 2.4 13.6-3.8 23.6-10.8 30.2-17.2C44 190.8 82 216 128 216c70.7 0 128-39.4 128-88s-57.3-88-128-88" /><circle fill="#fff" cx="84" cy="128" r="13" /><circle fill="#fff" cx="130" cy="128" r="13" /><circle fill="#fff" cx="176" cy="128" r="13" /></svg>

After

Width:  |  Height:  |  Size: 547 B

View File

@ -0,0 +1,79 @@
#!/bin/bash
# Rocket.Chat install hooks.
#
# Rocket.Chat needs its Mongo to be a replica set, because realtime message
# delivery works by tailing the oplog and a standalone mongod has no oplog. The
# official mongo image does not initiate a set on its own, so we do it once here
# after the containers come up.
rocketchat_install_post_start()
{
local app_name="$1"
((menu_number++))
echo ""
echo "---- $menu_number. Initiating the MongoDB replica set for $app_name"
echo ""
# Wait for mongod to answer at all. It has to be listening before
# rs.initiate() can be sent, and on a cold volume the first boot spends a
# while building journal files.
local attempts=0
while ((attempts < 45)); do
if runFileOp docker exec rocketchat-db mongosh --quiet --eval "db.adminCommand({ping:1}).ok" 2>/dev/null | grep -q 1; then
break
fi
sleep 2
((attempts++))
done
if ((attempts >= 45)); then
isError "rocketchat-db never started accepting connections — replica set not initiated."
isNotice "Rocket.Chat will keep restarting until it is. Once the database is up, run:"
isNotice " docker exec rocketchat-db mongosh --eval 'rs.initiate({_id:\"rs0\",members:[{_id:0,host:\"rocketchat-db:27017\"}]})'"
return 0
fi
# Idempotent: a reinstall over restored data, or a rerun of this hook, finds
# the set already configured and must not re-initiate it — rs.initiate() on
# a live set errors out and would leave the step looking failed.
if runFileOp docker exec rocketchat-db mongosh --quiet --eval "rs.status().ok" 2>/dev/null | grep -q 1; then
isSuccessful "MongoDB replica set rs0 already initiated — leaving it alone."
else
local result
result=$(runFileOp docker exec rocketchat-db mongosh --quiet --eval \
'rs.initiate({_id:"rs0",members:[{_id:0,host:"rocketchat-db:27017"}]})' 2>&1)
checkSuccess "Initiating MongoDB replica set rs0"
fi
# Election of the single member takes a moment. Rocket.Chat exits rather
# than retries if it connects before a primary exists, so wait for the node
# to report itself primary before handing control back.
attempts=0
while ((attempts < 30)); do
if runFileOp docker exec rocketchat-db mongosh --quiet --eval "db.hello().isWritablePrimary" 2>/dev/null | grep -q true; then
break
fi
sleep 2
((attempts++))
done
# Restart so Rocket.Chat reconnects immediately instead of waiting out its
# own backoff after the crash-loop it was in while the set was forming.
dockerComposeRestart "$app_name"
}
rocketchat_install_post()
{
local app_name="$1"
echo ""
isNotice "Rocket.Chat first run:"
echo ""
echo " Open the web interface and complete the setup wizard — the first"
echo " account you create there becomes the workspace administrator."
echo ""
echo " For voice and video, point Rocket.Chat at your Jitsi Meet server"
echo " under Admin -> Settings -> Video Conference -> Jitsi."
echo ""
}

View File

@ -0,0 +1,369 @@
# Stoat (formerly Revolt) — the closest open-source equivalent to Discord's
# model of servers, channels, roles and voice.
#
# Layout note, because it looks inconsistent at first glance: the compose
# *service keys* below (database, redis, api, autumn, ...) are deliberately kept
# identical to upstream's compose.yml, while container_name is prefixed with
# stoat- so nothing collides with other LibrePortal apps. Compose registers both
# the service key and the container name on the network, so upstream's internal
# defaults — the S3 endpoint baked into the file server, MINIO_DOMAIN, the
# service names in Revolt.toml — keep resolving, and LibrePortal still gets the
# prefixed container names its port, firewall and backup layers key on.
#
# 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
# upstream's own supported "behind another reverse proxy" mode — reimplementing
# eight path routes as Traefik labels would be a second copy to keep in sync for
# no benefit. Caddy is given HOSTNAME=:80 by the install hook so it serves plain
# HTTP and never tries to obtain its own certificate.
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
external: true
services:
# MongoDB — primary datastore.
#
# Upstream's healthcheck is kept rather than the LibrePortal HEALTHCHECK_TAG:
# half the stack uses `depends_on: condition: service_healthy` against it, so
# disabling the healthcheck would deadlock the boot order. Same for rabbit.
database:
container_name: stoat-database
image: mongo:8.0 #LIBREPORTAL|STOAT_MONGO_VERSION_TAG|8.0
restart: unless-stopped
volumes:
- ./data/db:/data/db
environment:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
interval: 10s
timeout: 10s
retries: 5
start_period: 10s
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_1 #LIBREPORTAL|IP_TAG_1|IP_DATA_1
# Valkey — event message broker and KV store.
redis:
container_name: stoat-redis
image: valkey/valkey:9-alpine #LIBREPORTAL|STOAT_VALKEY_VERSION_TAG|9-alpine
restart: unless-stopped
environment:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_2 #LIBREPORTAL|IP_TAG_2|IP_DATA_2
# RabbitMQ — internal message broker (push notifications, voice events).
rabbit:
container_name: stoat-rabbit
image: rabbitmq:4-alpine #LIBREPORTAL|STOAT_RABBITMQ_VERSION_TAG|4-alpine
restart: unless-stopped
environment:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
- RABBITMQ_DEFAULT_USER=stoat
- RABBITMQ_DEFAULT_PASS=PASSWORD_DATA_1 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1
volumes:
- ./data/rabbit:/var/lib/rabbitmq
healthcheck:
test: rabbitmq-diagnostics -q ping
interval: 10s
timeout: 10s
retries: 3
start_period: 20s
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_3 #LIBREPORTAL|IP_TAG_3|IP_DATA_3
# MinIO — S3-compatible object storage for uploads and avatars.
#
# The bucket-name aliases are load-bearing: the file server addresses
# buckets virtual-host style (<bucket>.minio), so without these the DNS
# lookup fails and every upload errors.
minio:
container_name: stoat-minio
image: minio/minio:latest #LIBREPORTAL|STOAT_MINIO_VERSION_TAG|latest
restart: unless-stopped
command: server /data
volumes:
- ./data/minio:/data
environment:
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
- MINIO_ROOT_USER=stoatminio
- MINIO_ROOT_PASSWORD=PASSWORD_DATA_2 #LIBREPORTAL|PASSWORD_TAG_2|PASSWORD_DATA_2
- MINIO_DOMAIN=minio
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_4 #LIBREPORTAL|IP_TAG_4|IP_DATA_4
aliases:
- minio
- revolt-uploads.minio
# Legacy bucket names, kept because instances created before
# the consolidation still address them.
- attachments.minio
- avatars.minio
- backgrounds.minio
- icons.minio
- banners.minio
- emojis.minio
# One-shot: creates the uploads bucket, then exits. Not a failure when you
# see it stopped.
createbuckets:
container_name: stoat-createbuckets
image: minio/mc:latest #LIBREPORTAL|STOAT_MINIO_MC_VERSION_TAG|latest
depends_on:
- minio
# Credentials come in through the environment rather than being written
# into the entrypoint: a #LIBREPORTAL annotation only substitutes on the
# line it sits on, and inside a folded block scalar it would end up as
# literal text in the command anyway. $$ escapes the dollar so compose
# leaves it for the container's shell instead of interpolating it here.
environment:
- MC_USER=stoatminio
- MC_PASS=PASSWORD_DATA_2 #LIBREPORTAL|PASSWORD_TAG_2|PASSWORD_DATA_2
entrypoint: >
/bin/sh -c "
while ! /usr/bin/mc ready minio; do
/usr/bin/mc alias set minio http://minio:9000 $$MC_USER $$MC_PASS;
echo 'Waiting minio...' && sleep 1;
done;
/usr/bin/mc mb --ignore-existing minio/revolt-uploads;
exit 0;
"
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_5 #LIBREPORTAL|IP_TAG_5|IP_DATA_5
# Caddy — internal path router for the whole stack. This is the only service
# Traefik talks to, and the only one carrying a Traefik router.
caddy: #LIBREPORTAL|SERVICE_TAG_1|caddy
container_name: stoat-caddy
image: caddy:2-alpine #LIBREPORTAL|STOAT_CADDY_VERSION_TAG|2-alpine
restart: unless-stopped
env_file: .env.web
# GLUETUN_OFF_BEGIN
ports:
- "PORTS_DATA_1" #LIBREPORTAL|PORTS_TAG_1|PORTS_DATA_1
# GLUETUN_OFF_END
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./stoat.json:/stoat.json:ro
- ./data/caddy-data:/data
- ./data/caddy-config:/config
labels:
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
libreportal.backup.db: "mongo:stoat-database:data/db:"
# Uploads live in MinIO, not on a filesystem the file server owns,
# so the object store's own data dir is what has to be captured.
libreportal.backup.files: "stoat-minio:/data:data/minio"
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
# TRAEFIK_PORT_1_BEGIN
traefik.http.routers.stoat-caddy.entrypoints: web,websecure
traefik.http.routers.stoat-caddy.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1
traefik.http.routers.stoat-caddy.tls: true
traefik.http.routers.stoat-caddy.tls.certresolver: production
traefik.http.services.stoat-caddy.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
traefik.http.routers.stoat-caddy.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1
# TRAEFIK_PORT_1_END
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
healthcheck:
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA
# GLUETUN_OFF_BEGIN
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_6 #LIBREPORTAL|IP_TAG_6|IP_DATA_6
aliases:
- caddy
# GLUETUN_OFF_END
# GLUETUN_ON_BEGIN
# network_mode: "container:gluetun-service"
# GLUETUN_ON_END
# API server.
api:
container_name: stoat-api
image: ghcr.io/stoatchat/api:v0.15.1 #LIBREPORTAL|STOAT_VERSION_TAG|v0.15.1
restart: unless-stopped
env_file: secrets.env
depends_on:
database:
condition: service_healthy
redis:
condition: service_started
rabbit:
condition: service_healthy
volumes:
- ./Revolt.toml:/Revolt.toml:ro
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_7 #LIBREPORTAL|IP_TAG_7|IP_DATA_7
aliases:
- api
# Websocket / events service.
events:
container_name: stoat-events
image: ghcr.io/stoatchat/events:v0.15.1 #LIBREPORTAL|STOAT_EVENTS_VERSION_TAG|v0.15.1
restart: unless-stopped
env_file: secrets.env
depends_on:
database:
condition: service_healthy
redis:
condition: service_started
volumes:
- ./Revolt.toml:/Revolt.toml:ro
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_8 #LIBREPORTAL|IP_TAG_8|IP_DATA_8
aliases:
- events
# Autumn — file server.
autumn:
container_name: stoat-autumn
image: ghcr.io/stoatchat/file-server:v0.15.1 #LIBREPORTAL|STOAT_AUTUMN_VERSION_TAG|v0.15.1
restart: unless-stopped
env_file: secrets.env
depends_on:
database:
condition: service_healthy
createbuckets:
condition: service_started
volumes:
- ./Revolt.toml:/Revolt.toml:ro
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_9 #LIBREPORTAL|IP_TAG_9|IP_DATA_9
aliases:
- autumn
# January — link metadata and image proxy.
january:
container_name: stoat-january
image: ghcr.io/stoatchat/proxy:v0.15.1 #LIBREPORTAL|STOAT_JANUARY_VERSION_TAG|v0.15.1
restart: unless-stopped
env_file: secrets.env
volumes:
- ./Revolt.toml:/Revolt.toml:ro
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_10 #LIBREPORTAL|IP_TAG_10|IP_DATA_10
aliases:
- january
# Gifbox — Tenor proxy for the GIF picker. Inert until a Tenor API key is
# added to secrets.env; see the upstream Guides.md.
gifbox:
container_name: stoat-gifbox
image: ghcr.io/stoatchat/gifbox:v0.15.1 #LIBREPORTAL|STOAT_GIFBOX_VERSION_TAG|v0.15.1
restart: unless-stopped
env_file: secrets.env
volumes:
- ./Revolt.toml:/Revolt.toml:ro
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_11 #LIBREPORTAL|IP_TAG_11|IP_DATA_11
aliases:
- gifbox
# Scheduled task daemon.
crond:
container_name: stoat-crond
image: ghcr.io/stoatchat/crond:v0.15.1 #LIBREPORTAL|STOAT_CROND_VERSION_TAG|v0.15.1
restart: unless-stopped
env_file: secrets.env
depends_on:
database:
condition: service_healthy
minio:
condition: service_started
volumes:
- ./Revolt.toml:/Revolt.toml:ro
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_12 #LIBREPORTAL|IP_TAG_12|IP_DATA_12
# Push notification daemon.
pushd:
container_name: stoat-pushd
image: ghcr.io/stoatchat/pushd:v0.15.1 #LIBREPORTAL|STOAT_PUSHD_VERSION_TAG|v0.15.1
restart: unless-stopped
env_file: secrets.env
depends_on:
database:
condition: service_healthy
redis:
condition: service_started
rabbit:
condition: service_healthy
volumes:
- ./Revolt.toml:/Revolt.toml:ro
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_13 #LIBREPORTAL|IP_TAG_13|IP_DATA_13
# Voice ingress daemon — receives LiveKit's webhooks.
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
restart: unless-stopped
env_file: secrets.env
depends_on:
database:
condition: service_healthy
rabbit:
condition: service_healthy
volumes:
- ./Revolt.toml:/Revolt.toml:ro
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_14 #LIBREPORTAL|IP_TAG_14|IP_DATA_14
aliases:
- voice-ingress
# LiveKit — the WebRTC SFU behind voice and video.
#
# Its media ports cannot go through Traefik: WebRTC is UDP, and Traefik is
# an HTTP proxy. The TCP fallback port is declared in stoat.config so the
# port and firewall layers manage it. The UDP range is published literally
# below because LibrePortal's port table stores one port per row and its
# firewall rebuild emits /tcp rules only — a range declared there would
# produce a wrong rule rather than no rule. Open it yourself if voice needs
# to work from outside the LAN:
# sudo ufw allow 50000:50100/udp
# Voice still falls back to TCP 7881 without it, at the cost of latency.
livekit:
container_name: stoat-livekit
image: ghcr.io/stoatchat/livekit-server:v1.9.13 #LIBREPORTAL|STOAT_LIVEKIT_VERSION_TAG|v1.9.13
restart: unless-stopped
command: --config /etc/livekit.yml
depends_on:
redis:
condition: service_started
ports:
- "PORTS_DATA_2" #LIBREPORTAL|PORTS_TAG_2|PORTS_DATA_2
- "50000-50100:50000-50100/udp"
volumes:
- ./livekit.yml:/etc/livekit.yml:ro
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_15 #LIBREPORTAL|IP_TAG_15|IP_DATA_15
aliases:
- livekit
# The web client itself. Served by Caddy at /.
web:
container_name: stoat-web
image: ghcr.io/stoatchat/for-web:0c31cf0 #LIBREPORTAL|STOAT_WEB_VERSION_TAG|0c31cf0
restart: unless-stopped
env_file: .env.web
networks:
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
ipv4_address: IP_DATA_16 #LIBREPORTAL|IP_TAG_16|IP_DATA_16
aliases:
- web

View File

@ -0,0 +1,70 @@
# Internal path router for the Stoat stack, taken from upstream's self-hosted
# repository. Traefik terminates TLS and proxies here, so HOSTNAME is set to
# ":80" in .env.web and Caddy neither requests nor serves certificates.
#
# The path prefixes below are not arbitrary the web client is built with
# VITE_API_URL=https://<domain>/api and friends, so these routes and the URLs in
# .env.web / Revolt.toml have to agree.
{$HOSTNAME} {
route /.well-known/stoat {
uri strip_prefix /.well-known/stoat
header {
Access-Control-Allow-Origin *
}
file_server {
root /stoat.json
}
}
route /api* {
uri strip_prefix /api
reverse_proxy http://api:14702 {
header_down Location "^/" "/api/"
}
}
route /ws {
uri strip_prefix /ws
reverse_proxy http://events:14703 {
header_down Location "^/" "/ws/"
}
}
route /autumn* {
uri strip_prefix /autumn
reverse_proxy http://autumn:14704 {
header_down Location "^/" "/autumn/"
}
}
route /january* {
uri strip_prefix /january
reverse_proxy http://january:14705 {
header_down Location "^/" "/january/"
}
}
route /gifbox* {
uri strip_prefix /gifbox
reverse_proxy http://gifbox:14706 {
header_down Location "^/" "/gifbox/"
}
}
route /livekit* {
uri strip_prefix /livekit
reverse_proxy http://livekit:7880 {
header_down Location "^/" "/livekit/"
}
}
route /ingress* {
uri strip_prefix /ingress
reverse_proxy http://voice-ingress:8500 {
header_down Location "^/" "/ingress/"
}
}
reverse_proxy http://web:5000
}

View File

@ -0,0 +1,226 @@
#!/bin/bash
# Stoat install hooks.
#
# Upstream configures an instance with an interactive generate_config.sh that
# asks for a domain and writes five files. This is the non-interactive
# equivalent, driven by the domain LibrePortal already knows and writing into
# the app's install directory.
#
# The one rule that matters here: secrets.env is generated ONCE and never
# rewritten. REVOLT__FILES__ENCRYPTION_KEY decrypts every file ever uploaded to
# the instance, so regenerating it on a reinstall would permanently orphan the
# entire media store — which is exactly the failure upstream's script warns
# about at length.
stoat_install_pre()
{
local app_name="$1"
if ! appInstallCheckRequirements "$app_name" "$CFG_STOAT_REQUIRES"; then
stoat=n
return 1
fi
}
# The public host, read back from the deployed compose once tag substitution has
# filled it in. Everything else in this file is derived from it.
_stoatDomain()
{
local app_name="$1"
tagsManagerGetTagContent "$containers_dir$app_name/docker-compose.yml" "DOMAINSUBNAME_TAG_1"
}
# Generate secrets.env if it does not already exist. Returns without touching an
# existing file — see the warning at the top.
_stoatWriteSecrets()
{
local secrets_file="$1"
if [[ -s "$secrets_file" ]]; then
isNotice "Existing secrets.env found — keeping it (regenerating would orphan every uploaded file)."
return 0
fi
# VAPID keypair for web push. The public key is the uncompressed EC point,
# which is the last 65 bytes of the DER encoding, base64url-encoded without
# padding — that is what the browser Push API expects.
local vapid_pem vapid_private vapid_public
vapid_pem=$(mktemp)
openssl ecparam -name prime256v1 -genkey -noout -out "$vapid_pem" 2>/dev/null
vapid_private=$(base64 < "$vapid_pem" | tr -d '\n' | tr -d '=')
vapid_public=$(openssl ec -in "$vapid_pem" -outform DER 2>/dev/null | tail -c 65 | base64 | tr '/+' '_-' | tr -d '\n' | tr -d '=')
rm -f "$vapid_pem"
local files_key livekit_key livekit_secret
files_key=$(openssl rand -base64 32)
livekit_key=$(openssl rand -hex 6)
livekit_secret=$(openssl rand -hex 24)
runFileWrite "$secrets_file" <<EOF
# Generated by LibrePortal at install time. Treat this file as you would a
# private key: REVOLT__FILES__ENCRYPTION_KEY is the only thing that can decrypt
# the media store, and it is never regenerated once written.
REVOLT__PUSHD__VAPID__PRIVATE_KEY='${vapid_private}'
REVOLT__PUSHD__VAPID__PUBLIC_KEY='${vapid_public}'
REVOLT__FILES__ENCRYPTION_KEY='${files_key}'
REVOLT__API__LIVEKIT__NODES__WORLDWIDE__KEY='${livekit_key}'
REVOLT__API__LIVEKIT__NODES__WORLDWIDE__SECRET='${livekit_secret}'
EOF
runFileOp chmod 600 "$secrets_file"
isSuccessful "Generated secrets.env"
}
stoat_install_post_compose()
{
local app_name="$1"
local app_dir="$containers_dir$app_name"
((menu_number++))
echo ""
echo "---- $menu_number. Generating the Stoat instance configuration"
echo ""
local domain
domain=$(_stoatDomain "$app_name")
if [[ -z "$domain" ]]; then
isError "Could not determine the public host from the compose file — aborting Stoat configuration."
isNotice "Check that CFG_STOAT_PORT_1 is public and Traefik-managed, then reinstall."
return 1
fi
local result
result=$(createFolders "loud" "$docker_install_user" \
"$app_dir/data/db" "$app_dir/data/rabbit" "$app_dir/data/minio" \
"$app_dir/data/caddy-data" "$app_dir/data/caddy-config")
checkSuccess "Creating $app_name data folders"
_stoatWriteSecrets "$app_dir/secrets.env"
# Read the LiveKit credentials back out — either the ones just generated or
# the ones preserved from a previous install — because livekit.yml has to
# carry the same pair the API is configured with.
local livekit_key livekit_secret
livekit_key=$(grep -oP "REVOLT__API__LIVEKIT__NODES__WORLDWIDE__KEY='\K[^']*" "$app_dir/secrets.env" 2>/dev/null)
livekit_secret=$(grep -oP "REVOLT__API__LIVEKIT__NODES__WORLDWIDE__SECRET='\K[^']*" "$app_dir/secrets.env" 2>/dev/null)
if [[ -z "$livekit_key" || -z "$livekit_secret" ]]; then
isError "Could not read the LiveKit credentials from secrets.env — voice will not work."
return 1
fi
# HOSTNAME=:80 is what puts Caddy in plain-HTTP mode behind Traefik. The
# VITE_* values are compiled into the browser bundle, so they must be the
# public https:// URLs, not internal container addresses.
local video_enabled=""
[[ "$CFG_STOAT_ENABLE_VIDEO" != "false" ]] && video_enabled="true"
runFileWrite "$app_dir/.env.web" <<EOF
HOSTNAME=:80
REVOLT_PUBLIC_URL=https://${domain}/api
VITE_API_URL=https://${domain}/api
VITE_WS_URL=wss://${domain}/ws
VITE_MEDIA_URL=https://${domain}/autumn
VITE_PROXY_URL=https://${domain}/january
VITE_GIFBOX_URL=https://${domain}/gifbox
VITE_CFG_ENABLE_VIDEO=${video_enabled}
EOF
checkSuccess "Writing .env.web for https://$domain"
# Client discovery document, served at /.well-known/stoat.
printf '{"api":"https://%s/api"}' "$domain" | runFileWrite "$app_dir/stoat.json"
checkSuccess "Writing stoat.json"
runFileWrite "$app_dir/Revolt.toml" <<EOF
# Generated by LibrePortal at install time. Secrets live in secrets.env, not
# here. Reinstalling the app rewrites this file — put custom configuration in a
# copy and merge it back if you change anything.
[hosts]
app = "https://${domain}"
api = "https://${domain}/api"
events = "wss://${domain}/ws"
autumn = "https://${domain}/autumn"
january = "https://${domain}/january"
gifbox = "https://${domain}/gifbox"
[hosts.livekit]
worldwide = "wss://${domain}/livekit"
[api.livekit.nodes.worldwide]
url = "http://livekit:7880"
lat = 0.0
lon = 0.0
EOF
if [[ -n "$video_enabled" ]]; then
runFileWrite -a "$app_dir/Revolt.toml" <<'EOF'
[features.limits.new_user]
video_resolution = [1920, 1080]
video_aspect_ratio = [0.3, 10]
[features.limits.default]
video_resolution = [1920, 1080]
video_aspect_ratio = [0.3, 10]
EOF
fi
checkSuccess "Writing Revolt.toml (video=${video_enabled:-false})"
# use_external_ip lets LiveKit discover the address to advertise for WebRTC.
# The port range matches the literal UDP mapping in the compose file; change
# one and you must change the other.
runFileWrite "$app_dir/livekit.yml" <<EOF
rtc:
use_external_ip: true
port_range_start: 50000
port_range_end: 50100
tcp_port: 7881
redis:
address: redis:6379
turn:
enabled: false
keys:
${livekit_key}: ${livekit_secret}
webhook:
api_key: ${livekit_key}
urls:
- "http://voice-ingress:8500/worldwide"
EOF
checkSuccess "Writing livekit.yml"
result=$(copyResource "$app_name" "Caddyfile" "" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying Caddyfile to $app_dir"
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$app_dir"
checkSuccess "Setting ownership on the $app_name install directory"
}
stoat_install_post()
{
local app_name="$1"
local domain
domain=$(_stoatDomain "$app_name")
echo ""
isNotice "Stoat first run:"
echo ""
echo " Open https://${domain} and create an account — the first account"
echo " registered on a fresh instance becomes the instance owner."
echo ""
echo " Give it a few minutes on first boot: sixteen containers start in"
echo " dependency order, and the API restarts until MongoDB and RabbitMQ"
echo " both report healthy. 'docker compose ps' in the app directory"
echo " shows where it has got to."
echo ""
echo " Voice falls back to TCP 7881, which is already open. For proper"
echo " low-latency WebRTC from outside your LAN, also allow the UDP"
echo " media range — LibrePortal's firewall layer only emits TCP rules,"
echo " so this one is manual:"
echo ""
echo " sudo ufw allow 50000:50100/udp"
echo ""
}

View File

@ -0,0 +1,87 @@
#
# =============================================================================
# GENERAL CONFIGURATION
# =============================================================================
# APP_NAME = name of application for use in scripts
# REQUIRES = comma-separated install prerequisites (see scripts/checks/requirements/check_app_install.sh)
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
# BACKUP = if true, include this application in backup operations
# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
# AUTHELIA = if true, use Authelia authentication, if false turned off.
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
# ENABLE_VIDEO = if true, allow camera and screen sharing (voice always works)
# MONITORING = if true, export this app's metrics to Prometheus + Grafana (needs both apps installed)
#
CFG_STOAT_APP_NAME=stoat
# Stoat bakes its public URL into the client bundle and into Revolt.toml at
# install time, and voice needs real TLS, so a domain behind Traefik is a
# prerequisite rather than a nicety.
CFG_STOAT_REQUIRES="domain,traefik"
CFG_STOAT_BACKUP=true
CFG_STOAT_BACKUP_STRATEGY=auto
# Manual, deliberately. This is a sixteen-service stack whose components are
# released together and expect matching versions; letting them roll forward
# unattended and independently is how you end up with an API talking to an
# incompatible events service.
CFG_STOAT_UPDATE_TYPE=manual
CFG_STOAT_COMPOSE_FILE=default
CFG_STOAT_HEALTHCHECK=true
# Stoat's own accounts back its clients, and /api must stay reachable without a
# forward-auth redirect in the way.
CFG_STOAT_AUTHELIA=false
CFG_STOAT_HEADSCALE=false
CFG_STOAT_ENABLE_VIDEO=true
CFG_STOAT_MONITORING=false
#
# =============================================================================
# METADATA
# =============================================================================
# CATEGORY = application category for grouping
# TITLE = display name for the application
# DESCRIPTION = short description of the application
# LONG_DESCRIPTION = detailed description of the application
# URL = source repository or documentation URL
# ACTIONS = available actions for this application
# REQUIRES_SERVICE = name of another LibrePortal app that must be installed before this one can be configured
#
CFG_STOAT_CATEGORY="communication"
CFG_STOAT_TITLE="Stoat"
CFG_STOAT_DESCRIPTION="Discord-style Chat"
CFG_STOAT_LONG_DESCRIPTION="Stoat, formerly Revolt, is the open-source project that most closely reproduces Discord itself — servers, channels, roles, reactions, and voice and video through LiveKit. It is the heaviest app in this catalog by some distance: sixteen containers including MongoDB, Valkey, RabbitMQ and MinIO, so budget a couple of gigabytes of memory. Unlike Matrix it does not federate, so each instance is its own island"
CFG_STOAT_URL="https://github.com/stoatchat/self-hosted"
CFG_STOAT_ACTIONS="configure|install|restart|shutdown|uninstall"
CFG_STOAT_REQUIRES_SERVICE=traefik
#
# =============================================================================
# NETWORK CONFIGURATION
# =============================================================================
# DOMAIN = number of domain from the general config, useful when using multiple domains
# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all
#
CFG_STOAT_DOMAIN=1
CFG_STOAT_WHITELIST=false
CFG_STOAT_NETWORK=default
#
# =============================================================================
# PORT CONFIGURATION
# =============================================================================
# PORT_ = port configuration: app|name|external:internal|access|protocol|login|traefik|webui|description
# - app: application name
# - name: service identifier (webui, dns, ssh, etc.)
# - external:internal: port mapping (external can be 'random' for auto-allocation)
# - access: 'public' (internet accessible), 'private' (local network only), 'disabled' (not running)
# - protocol: 'tcp' or 'udp'
# - login: if true, this port requires basic-auth via Traefik (only meaningful when traefik=true)
# - traefik: if true, Traefik handles this port (reverse proxy)
# - webui: if true, this port serves the main web interface
# - description: human-readable description of the service
#
# Only one HTTP port: Caddy fronts the entire stack internally, so /api, /ws,
# /autumn and the rest all arrive on this single host.
CFG_STOAT_PORT_1="stoat-caddy|webui|random:80|public|tcp|false|true|true|Web Interface||stoat"
# LiveKit's TCP fallback. Pinned rather than random on purpose: LiveKit
# advertises this exact port number to clients from livekit.yml, so a randomised
# external port would be advertised wrongly and voice would fail to connect.
# Not Traefik-managed — WebRTC is not HTTP.
CFG_STOAT_PORT_2="stoat-livekit|voice-tcp|7881:7881|public|tcp|false|false|false|LiveKit voice/video (TCP fallback)|"

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256" role="img" aria-label="Stoat"><rect width="256" height="256" rx="28" fill="#ff4655" /><path fill="#fff" d="M56 64h56a52 52 0 0 1 0 104H92v34a6 6 0 0 1-6 6H62a6 6 0 0 1-6-6zm36 28v48h20a24 24 0 0 0 0-48z" /><circle fill="#fff" cx="176" cy="96" r="16" /><circle fill="#fff" cx="176" cy="152" r="16" /><circle fill="#fff" cx="176" cy="208" r="16" /></svg>

After

Width:  |  Height:  |  Size: 440 B

View File

@ -17,7 +17,7 @@
# labels:
# libreportal.backup.db: "<kind>:<container>:<datadir>:<path>"
#
# kind mysql | mariadb | postgres | sqlite
# kind mysql | mariadb | postgres | mongo | sqlite
# container service container_name to `docker exec` into (server engines)
# datadir app-dir-relative folder holding raw DB files, excluded on live
# path app-dir-relative path to the sqlite file (sqlite only)
@ -25,6 +25,7 @@
# Examples:
# "mysql:nextcloud-db:db_data:" MariaDB/MySQL in nextcloud-db, raw db_data/ excluded
# "postgres:mastodon-db:postgres_data:" Postgres in mastodon-db
# "mongo:rocketchat-db:mongo_data:" MongoDB in rocketchat-db
# "sqlite:::data/gitea.db" sqlite file at data/gitea.db
#
# An app with no database can still opt into live snapshots (its files are
@ -130,10 +131,19 @@ _backupDbDumpName()
local kind="$1" container="$2" path="$3"
case "$kind" in
sqlite) echo "sqlite-$(echo "$path" | tr '/' '_').sqlite.gz" ;;
# mongodump emits a binary archive, not SQL text — name it honestly so a
# human poking at .lp-backup/db doesn't try to `zcat | psql` it.
mongo) echo "db-${container}.archive.gz" ;;
*) echo "db-${container}.sql.gz" ;;
esac
}
# Positional-arg preamble for the mongo tools: sets "$@" to the credential flags
# when the container was started with root auth, and to nothing when it wasn't.
# Built with `set --` rather than a flat string so a password containing spaces
# or globbing characters survives word splitting intact.
_backup_mongo_auth_sh='if [ -n "${MONGO_INITDB_ROOT_USERNAME:-}" ]; then set -- -u "$MONGO_INITDB_ROOT_USERNAME" -p "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin; else set --; fi;'
# Wait until a server database is genuinely ready for a load. On a fresh init
# (the restore case) the engine starts a throwaway temp server, runs its setup,
# then stops it and starts the real one — a simple ping passes against the temp
@ -149,6 +159,11 @@ _backupDbWaitReady()
case "$kind" in
postgres)
runFileOp docker exec "$container" sh -c 'export PGPASSWORD="${POSTGRES_PASSWORD:-}"; psql -U "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DB:-${POSTGRES_USER:-postgres}}" -tAc "SELECT 1"' >/dev/null 2>&1 && good=1 ;;
mongo)
# mongosh on 6.0+, the legacy mongo shell on older images. A
# ping that returns ok:1 means the node is past init AND (for a
# replica set, which Rocket.Chat requires) has a primary.
runFileOp docker exec "$container" sh -c "$_backup_mongo_auth_sh"' (mongosh "$@" --quiet --eval "db.adminCommand({ping:1}).ok" 2>/dev/null || mongo "$@" --quiet --eval "db.adminCommand({ping:1}).ok" 2>/dev/null) | grep -q 1' >/dev/null 2>&1 && good=1 ;;
*)
runFileOp docker exec "$container" sh -c 'RP="${MARIADB_ROOT_PASSWORD:-$MYSQL_ROOT_PASSWORD}"; mariadb -uroot -p"$RP" -N -e "SELECT 1" 2>/dev/null || mysql -uroot -p"$RP" -N -e "SELECT 1"' >/dev/null 2>&1 && good=1 ;;
esac
@ -172,6 +187,12 @@ _backupDbImport()
postgres)
runFileOp gzip -dc "$dump" | docker exec -i "$container" sh -c \
'export PGPASSWORD="${POSTGRES_PASSWORD:-}"; psql -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DB:-${POSTGRES_USER:-postgres}}"' >/dev/null 2>&1 ;;
mongo)
# --drop replaces each collection as it is restored, which is what
# makes a re-run idempotent (the retry loop in the caller depends on
# that, exactly like pg_dump --clean --if-exists).
runFileOp gzip -dc "$dump" | docker exec -i "$container" sh -c \
"$_backup_mongo_auth_sh"' mongorestore "$@" --archive --drop --quiet' >/dev/null 2>&1 ;;
*)
runFileOp gzip -dc "$dump" | docker exec -i "$container" sh -c \
'RP="${MARIADB_ROOT_PASSWORD:-$MYSQL_ROOT_PASSWORD}"; (mariadb -uroot -p"$RP" 2>/dev/null || mysql -uroot -p"$RP")' >/dev/null 2>&1 ;;
@ -217,6 +238,18 @@ backupDbDump()
isError "$kind dump failed ($container)"; rc=1
fi
;;
mongo)
isNotice "Dumping mongo ($container) — live, consistent"
# No --gzip on mongodump: the pipeline below already gzips, and
# compressing twice just burns CPU for nothing.
if runFileOp docker exec "$container" sh -c \
"$_backup_mongo_auth_sh"' mongodump "$@" --archive --quiet' \
2>/dev/null | gzip | runFileWrite "$dump"; then
isSuccessful "mongo dump written ($container)"
else
isError "mongo dump failed ($container)"; rc=1
fi
;;
sqlite)
isNotice "Dumping sqlite ($path) — live, consistent"
local src="$app_dir/$path"

View File

@ -646,6 +646,11 @@ declare -gA LP_FN_MAP=(
[manifestRemove]="backup/manifest/manifest_write.sh"
[manifestWrite]="backup/manifest/manifest_write.sh"
[mastodon_upgrade_verify]="mastodon/scripts/mastodon_upgrade_hooks.sh"
[matrix_install_post]="matrix/scripts/matrix_install_hooks.sh"
[matrix_install_post_compose]="matrix/scripts/matrix_install_hooks.sh"
[matrix_install_post_start]="matrix/scripts/matrix_install_hooks.sh"
[matrix_install_pre]="matrix/scripts/matrix_install_hooks.sh"
[_matrixServerName]="matrix/scripts/matrix_install_hooks.sh"
[mattermostToolsMenu]="menu/tools/manage_mattermost.sh"
[maybeRegenPoll]="task/crontab_task_processor.sh"
[menuContinue]="menu/message/continue.sh"
@ -815,6 +820,8 @@ declare -gA LP_FN_MAP=(
[restoreFirstRunBulk]="restore/restore_first_run.sh"
[restoreFirstRunDiscover]="restore/restore_first_run.sh"
[restorePickSnapshot]="restore/restore_app_pick.sh"
[rocketchat_install_post]="rocketchat/scripts/rocketchat_install_hooks.sh"
[rocketchat_install_post_start]="rocketchat/scripts/rocketchat_install_hooks.sh"
[runAppCfg]="docker/command/run_privileged.sh"
[runAsManager]="docker/command/run_privileged.sh"
[runBackupOp]="docker/command/run_privileged.sh"
@ -868,15 +875,20 @@ declare -gA LP_FN_MAP=(
[stalwart_install_message_data]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_install_post_start]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_install_provision]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_wait_http]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_upgrade_admin_ui_code]="stalwart/scripts/stalwart_upgrade_hooks.sh"
[stalwart_upgrade_check_admin_ui]="stalwart/scripts/stalwart_upgrade_hooks.sh"
[stalwart_upgrade_verify]="stalwart/scripts/stalwart_upgrade_hooks.sh"
[stalwart_wait_http]="stalwart/scripts/stalwart_install_hooks.sh"
[startInstall]="start/start_install.sh"
[startLoad]="start/start_load.sh"
[startOther]="start/start_other.sh"
[startPreInstall]="start/start_preinstall.sh"
[startScan]="start/start_scan.sh"
[_stoatDomain]="stoat/scripts/stoat_install_hooks.sh"
[stoat_install_post]="stoat/scripts/stoat_install_hooks.sh"
[stoat_install_post_compose]="stoat/scripts/stoat_install_hooks.sh"
[stoat_install_pre]="stoat/scripts/stoat_install_hooks.sh"
[_stoatWriteSecrets]="stoat/scripts/stoat_install_hooks.sh"
[stopCrowdsec]="crowdsec/crowdsec.sh"
[switchMigrateBackupApps]="docker/type_switcher/swap_docker_type.sh"
[switchMigrateRestoreApps]="docker/type_switcher/swap_docker_type.sh"
@ -1689,6 +1701,11 @@ declare -gA LP_FN_ROOT=(
[manifestRemove]="scripts"
[manifestWrite]="scripts"
[mastodon_upgrade_verify]="containers"
[matrix_install_post]="containers"
[matrix_install_post_compose]="containers"
[matrix_install_post_start]="containers"
[matrix_install_pre]="containers"
[_matrixServerName]="containers"
[mattermostToolsMenu]="scripts"
[maybeRegenPoll]="scripts"
[menuContinue]="scripts"
@ -1858,6 +1875,8 @@ declare -gA LP_FN_ROOT=(
[restoreFirstRunBulk]="scripts"
[restoreFirstRunDiscover]="scripts"
[restorePickSnapshot]="scripts"
[rocketchat_install_post]="containers"
[rocketchat_install_post_start]="containers"
[runAppCfg]="scripts"
[runAsManager]="scripts"
[runBackupOp]="scripts"
@ -1911,15 +1930,20 @@ declare -gA LP_FN_ROOT=(
[stalwart_install_message_data]="containers"
[stalwart_install_post_start]="containers"
[stalwart_install_provision]="containers"
[stalwart_wait_http]="containers"
[stalwart_upgrade_admin_ui_code]="containers"
[stalwart_upgrade_check_admin_ui]="containers"
[stalwart_upgrade_verify]="containers"
[stalwart_wait_http]="containers"
[startInstall]="scripts"
[startLoad]="scripts"
[startOther]="scripts"
[startPreInstall]="scripts"
[startScan]="scripts"
[_stoatDomain]="containers"
[stoat_install_post]="containers"
[stoat_install_post_compose]="containers"
[stoat_install_pre]="containers"
[_stoatWriteSecrets]="containers"
[stopCrowdsec]="containers"
[switchMigrateBackupApps]="scripts"
[switchMigrateRestoreApps]="scripts"
@ -2766,6 +2790,11 @@ manifestReadFromSnapshot() { unset -f manifestReadFromSnapshot; __lpAutoload "${
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 "$@"; }
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_compose() { unset -f matrix_install_post_compose; __lpAutoload "${install_containers_dir}matrix/scripts/matrix_install_hooks.sh"; matrix_install_post_compose "$@"; }
matrix_install_post_start() { unset -f matrix_install_post_start; __lpAutoload "${install_containers_dir}matrix/scripts/matrix_install_hooks.sh"; matrix_install_post_start "$@"; }
matrix_install_pre() { unset -f matrix_install_pre; __lpAutoload "${install_containers_dir}matrix/scripts/matrix_install_hooks.sh"; matrix_install_pre "$@"; }
_matrixServerName() { unset -f _matrixServerName; __lpAutoload "${install_containers_dir}matrix/scripts/matrix_install_hooks.sh"; _matrixServerName "$@"; }
mattermostToolsMenu() { unset -f mattermostToolsMenu; __lpAutoload "${install_scripts_dir}menu/tools/manage_mattermost.sh"; mattermostToolsMenu "$@"; }
maybeRegenPoll() { unset -f maybeRegenPoll; __lpAutoload "${install_scripts_dir}task/crontab_task_processor.sh"; maybeRegenPoll "$@"; }
menuContinue() { unset -f menuContinue; __lpAutoload "${install_scripts_dir}menu/message/continue.sh"; menuContinue "$@"; }
@ -2935,6 +2964,8 @@ restoreFilesRehydratePreStart() { unset -f restoreFilesRehydratePreStart; __lpAu
restoreFirstRunBulk() { unset -f restoreFirstRunBulk; __lpAutoload "${install_scripts_dir}restore/restore_first_run.sh"; restoreFirstRunBulk "$@"; }
restoreFirstRunDiscover() { unset -f restoreFirstRunDiscover; __lpAutoload "${install_scripts_dir}restore/restore_first_run.sh"; restoreFirstRunDiscover "$@"; }
restorePickSnapshot() { unset -f restorePickSnapshot; __lpAutoload "${install_scripts_dir}restore/restore_app_pick.sh"; restorePickSnapshot "$@"; }
rocketchat_install_post() { unset -f rocketchat_install_post; __lpAutoload "${install_containers_dir}rocketchat/scripts/rocketchat_install_hooks.sh"; rocketchat_install_post "$@"; }
rocketchat_install_post_start() { unset -f rocketchat_install_post_start; __lpAutoload "${install_containers_dir}rocketchat/scripts/rocketchat_install_hooks.sh"; rocketchat_install_post_start "$@"; }
runAppCfg() { unset -f runAppCfg; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runAppCfg "$@"; }
runAsManager() { unset -f runAsManager; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runAsManager "$@"; }
runBackupOp() { unset -f runBackupOp; __lpAutoload "${install_scripts_dir}docker/command/run_privileged.sh"; runBackupOp "$@"; }
@ -2982,16 +3013,26 @@ setupWizardTerminal() { unset -f setupWizardTerminal; __lpAutoload "${install_sc
showInstructions() { unset -f showInstructions; __lpAutoload "${install_scripts_dir}menu/message/instructions.sh"; showInstructions "$@"; }
sourceBackupLocations() { unset -f sourceBackupLocations; __lpAutoload "${install_scripts_dir}backup/locations/location_loader.sh"; sourceBackupLocations "$@"; }
sshRemote() { unset -f sshRemote; __lpAutoload "${install_scripts_dir}network/ssh/ssh.sh"; sshRemote "$@"; }
stalwart_cli() { unset -f stalwart_cli; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_cli "$@"; }
stalwart_install_dns_provider() { unset -f stalwart_install_dns_provider; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_install_dns_provider "$@"; }
stalwart_install_first_mailbox() { unset -f stalwart_install_first_mailbox; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_install_first_mailbox "$@"; }
stalwart_install_message_data() { unset -f stalwart_install_message_data; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_install_message_data "$@"; }
stalwart_install_post_start() { unset -f stalwart_install_post_start; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_install_post_start "$@"; }
stalwart_install_provision() { unset -f stalwart_install_provision; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_install_provision "$@"; }
stalwart_upgrade_admin_ui_code() { unset -f stalwart_upgrade_admin_ui_code; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_upgrade_hooks.sh"; stalwart_upgrade_admin_ui_code "$@"; }
stalwart_upgrade_check_admin_ui() { unset -f stalwart_upgrade_check_admin_ui; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_upgrade_hooks.sh"; stalwart_upgrade_check_admin_ui "$@"; }
stalwart_upgrade_verify() { unset -f stalwart_upgrade_verify; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_upgrade_hooks.sh"; stalwart_upgrade_verify "$@"; }
stalwart_wait_http() { unset -f stalwart_wait_http; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_wait_http "$@"; }
startInstall() { unset -f startInstall; __lpAutoload "${install_scripts_dir}start/start_install.sh"; startInstall "$@"; }
startLoad() { unset -f startLoad; __lpAutoload "${install_scripts_dir}start/start_load.sh"; startLoad "$@"; }
startOther() { unset -f startOther; __lpAutoload "${install_scripts_dir}start/start_other.sh"; startOther "$@"; }
startPreInstall() { unset -f startPreInstall; __lpAutoload "${install_scripts_dir}start/start_preinstall.sh"; startPreInstall "$@"; }
startScan() { unset -f startScan; __lpAutoload "${install_scripts_dir}start/start_scan.sh"; startScan "$@"; }
_stoatDomain() { unset -f _stoatDomain; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; _stoatDomain "$@"; }
stoat_install_post() { unset -f stoat_install_post; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; stoat_install_post "$@"; }
stoat_install_post_compose() { unset -f stoat_install_post_compose; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; stoat_install_post_compose "$@"; }
stoat_install_pre() { unset -f stoat_install_pre; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; stoat_install_pre "$@"; }
_stoatWriteSecrets() { unset -f _stoatWriteSecrets; __lpAutoload "${install_containers_dir}stoat/scripts/stoat_install_hooks.sh"; _stoatWriteSecrets "$@"; }
stopCrowdsec() { unset -f stopCrowdsec; __lpAutoload "${install_containers_dir}crowdsec/crowdsec.sh"; stopCrowdsec "$@"; }
switchMigrateBackupApps() { unset -f switchMigrateBackupApps; __lpAutoload "${install_scripts_dir}docker/type_switcher/swap_docker_type.sh"; switchMigrateBackupApps "$@"; }
switchMigrateRestoreApps() { unset -f switchMigrateRestoreApps; __lpAutoload "${install_scripts_dir}docker/type_switcher/swap_docker_type.sh"; switchMigrateRestoreApps "$@"; }