diff --git a/containers/stoat/docker-compose.yml b/containers/stoat/docker-compose.yml new file mode 100644 index 0000000..aa7e624 --- /dev/null +++ b/containers/stoat/docker-compose.yml @@ -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 (.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 diff --git a/containers/stoat/resources/Caddyfile b/containers/stoat/resources/Caddyfile new file mode 100644 index 0000000..98ab4bd --- /dev/null +++ b/containers/stoat/resources/Caddyfile @@ -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:///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 +} diff --git a/containers/stoat/scripts/stoat_install_hooks.sh b/containers/stoat/scripts/stoat_install_hooks.sh new file mode 100644 index 0000000..7ad50d1 --- /dev/null +++ b/containers/stoat/scripts/stoat_install_hooks.sh @@ -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" </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" <&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 "" +} diff --git a/containers/stoat/stoat.config b/containers/stoat/stoat.config new file mode 100644 index 0000000..5863383 --- /dev/null +++ b/containers/stoat/stoat.config @@ -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)|" diff --git a/containers/stoat/stoat.svg b/containers/stoat/stoat.svg new file mode 100644 index 0000000..473c00c --- /dev/null +++ b/containers/stoat/stoat.svg @@ -0,0 +1 @@ + diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index d5f4dec..86b70a2 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -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 "$@"; }