LibrePortal/containers/mattermost/docker-compose.yml
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

93 lines
5.3 KiB
YAML

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