Installing rocketchat failed with
invalid IPv4 address: ParseAddr("IP_DATA_2"): unable to parse IP
ipUpdateComposeTags allocates one IP per SERVICE_TAG_N annotation and fills
IP_TAG_i only where SERVICE_TAG_i exists. The four new apps tagged only their
primary service, so every sidecar — matrix's postgres, mattermost's postgres,
rocketchat's mongo, and fifteen of stoat's sixteen — kept a literal IP_DATA_n
in the deployed compose and docker refused to create the container.
Tag every service that carries an ipv4_address, index-aligned with its IP_TAG.
For stoat that also meant moving caddy from SERVICE_TAG_1 to _6 so the indices
line up with the IPs rather than the reading order.
mastodon had the same latent break (IP_TAG_2 and _3 untagged) and is fixed the
same way — it would have failed on first install for the same reason.
SERVICE_TAG carries the compose *key*, not container_name: 'libreportal app
restart <app> <service>' passes it to 'docker compose restart', which only
understands keys.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
384 lines
17 KiB
YAML
384 lines
17 KiB
YAML
# 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.
|
|
#
|
|
# SERVICE_TAG_n carries the compose *key*, not the container name, for two
|
|
# reasons: ipUpdateComposeTags allocates exactly one IP per SERVICE_TAG and fills
|
|
# IP_TAG_i only where SERVICE_TAG_i exists (so every service with an
|
|
# ipv4_address needs one, index-aligned, or it deploys with a literal
|
|
# IP_DATA_n and compose refuses to start), and `libreportal app restart <app>
|
|
# <service>` passes the value straight to `docker compose restart`, which only
|
|
# understands keys.
|
|
#
|
|
# Upstream fronts the whole stack with Caddy doing path routing (/api, /ws,
|
|
# /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: #LIBREPORTAL|SERVICE_TAG_1|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
|
|
# Required on Linux 6.19 and newer — see SERVER-121912. MongoDB 8.x
|
|
# bundles a TCMalloc whose restartable-sequence registration the
|
|
# newer kernel rejects, and mongod aborts on startup rather than
|
|
# degrading. Letting glibc own the rseq registration sidesteps it.
|
|
# Upstream Stoat hit the same wall (stoatchat/self-hosted#268).
|
|
- GLIBC_TUNABLES=glibc.pthread.rseq=1
|
|
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: #LIBREPORTAL|SERVICE_TAG_2|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: #LIBREPORTAL|SERVICE_TAG_3|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=STOAT_RABBITMQ_PASSWORD_1_DATA #LIBREPORTAL|STOAT_RABBITMQ_PASSWORD_1_TAG|STOAT_RABBITMQ_PASSWORD_1_DATA
|
|
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: #LIBREPORTAL|SERVICE_TAG_4|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=STOAT_MINIO_PASSWORD_1_DATA #LIBREPORTAL|STOAT_MINIO_PASSWORD_1_TAG|STOAT_MINIO_PASSWORD_1_DATA
|
|
- 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: #LIBREPORTAL|SERVICE_TAG_5|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=STOAT_MINIO_PASSWORD_1_DATA #LIBREPORTAL|STOAT_MINIO_PASSWORD_1_TAG|STOAT_MINIO_PASSWORD_1_DATA
|
|
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_6|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: #LIBREPORTAL|SERVICE_TAG_7|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: #LIBREPORTAL|SERVICE_TAG_8|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: #LIBREPORTAL|SERVICE_TAG_9|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: #LIBREPORTAL|SERVICE_TAG_10|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: #LIBREPORTAL|SERVICE_TAG_11|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: #LIBREPORTAL|SERVICE_TAG_12|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: #LIBREPORTAL|SERVICE_TAG_13|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: #LIBREPORTAL|SERVICE_TAG_14|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: #LIBREPORTAL|SERVICE_TAG_15|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: #LIBREPORTAL|SERVICE_TAG_16|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
|