# 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 # ` 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. stoat-database: #LIBREPORTAL|SERVICE_TAG_1|stoat-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 aliases: - database # Valkey — event message broker and KV store. stoat-redis: #LIBREPORTAL|SERVICE_TAG_2|stoat-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 aliases: - redis # RabbitMQ — internal message broker (push notifications, voice events). stoat-rabbit: #LIBREPORTAL|SERVICE_TAG_3|stoat-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 aliases: - rabbit # 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. stoat-minio: #LIBREPORTAL|SERVICE_TAG_4|stoat-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. stoat-createbuckets: #LIBREPORTAL|SERVICE_TAG_5|stoat-createbuckets container_name: stoat-createbuckets image: minio/mc:latest #LIBREPORTAL|STOAT_MINIO_MC_VERSION_TAG|latest depends_on: - stoat-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 aliases: - createbuckets # Caddy — internal path router for the whole stack. This is the only service # Traefik talks to, and the only one carrying a Traefik router. stoat-caddy: #LIBREPORTAL|SERVICE_TAG_6|stoat-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. stoat-api: #LIBREPORTAL|SERVICE_TAG_7|stoat-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: stoat-database: condition: service_healthy stoat-redis: condition: service_started stoat-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. stoat-events: #LIBREPORTAL|SERVICE_TAG_8|stoat-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: stoat-database: condition: service_healthy stoat-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. stoat-autumn: #LIBREPORTAL|SERVICE_TAG_9|stoat-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: stoat-database: condition: service_healthy stoat-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. stoat-january: #LIBREPORTAL|SERVICE_TAG_10|stoat-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. stoat-gifbox: #LIBREPORTAL|SERVICE_TAG_11|stoat-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. stoat-crond: #LIBREPORTAL|SERVICE_TAG_12|stoat-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: stoat-database: condition: service_healthy stoat-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 aliases: - crond # Push notification daemon. stoat-pushd: #LIBREPORTAL|SERVICE_TAG_13|stoat-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: stoat-database: condition: service_healthy stoat-redis: condition: service_started stoat-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 aliases: - pushd # Voice ingress daemon — receives LiveKit's webhooks. stoat-voice-ingress: #LIBREPORTAL|SERVICE_TAG_14|stoat-voice-ingress container_name: stoat-voice-ingress image: ghcr.io/stoatchat/voice-ingress:v0.15.1 #LIBREPORTAL|STOAT_VOICE_INGRESS_VERSION_TAG|v0.15.1 restart: unless-stopped env_file: secrets.env depends_on: stoat-database: condition: service_healthy stoat-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. stoat-livekit: #LIBREPORTAL|SERVICE_TAG_15|stoat-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: stoat-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 /. stoat-web: #LIBREPORTAL|SERVICE_TAG_16|stoat-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