# Image pins live HERE, as interpolation defaults, so `git pull` (or # re-downloading this file) delivers the exact version combination this # repository has tested — no .env editing required. Setting the matching # variable in .env overrides the default for anyone who wants to pin # differently; .env itself carries only secrets and deliberate overrides. # CI (Trivy scans, check-pin-freshness, the ephemeral test .env) reads # the pins from these lines — this block is the single source of truth. x-images: traefik: &traefik-image ${TRAEFIK_IMAGE_TAG:-traefik:${TRAEFIK_IMAGE_VERSION:-3.7@sha256:f4bb143d1135a579b630ed491e17ec50f6f7c9c6a30585ff3d385471d2a86f5a}} postgres: &postgres-image ${MATTERMOST_POSTGRES_IMAGE_TAG:-postgres:${MATTERMOST_POSTGRES_IMAGE_VERSION:-16@sha256:f9a7b9ebf6f56201f949db745e47bc423c0f0f8294bafe991cc10bf74b029657}} mattermost: &mattermost-image ${MATTERMOST_IMAGE_TAG:-mattermost/mattermost-team-edition:${MATTERMOST_IMAGE_VERSION:-12.0.2@sha256:0e435f3397390c36797d0c09b6fcfd73841ebf89bb292d6c6e109082a21ad7b6}} networks: mattermost-network: external: true traefik-network: external: true volumes: mattermost-data: mattermost-config: mattermost-logs: mattermost-plugins: mattermost-client-plugins: mattermost-bleve-indexes: mattermost-postgres: mattermost-postgres-backup: mattermost-data-backups: mattermost-database-backups: traefik-certificates: services: postgres: image: *postgres-image # Hardening: no privilege escalation via setuid binaries security_opt: - no-new-privileges:true # Drop every Linux capability; add back only what this image's # entrypoint needs (bind :80/:443, chown its data directory, drop to # its service user). cap_drop: - ALL cap_add: - CHOWN - DAC_READ_SEARCH - FOWNER - SETGID - SETUID # Resource limits as .env-overridable defaults (e.g. MATTERMOST_POSTGRES_MEMORY_LIMIT=2g); # the override survives every git pull. # Docker's default is ten seconds, then SIGKILL. That is not always enough # for this image to finish shutting down: PostgreSQL has a checkpoint to # write, MariaDB has InnoDB to flush, and Redis saves its dataset on the way # out. Killed halfway, the next start does crash recovery, and a Redis that # held another application's file locks leaves them behind for a person to # clear by hand. Sixty seconds, overridable in .env. stop_grace_period: ${MATTERMOST_POSTGRES_STOP_GRACE_PERIOD:-60s} deploy: resources: limits: memory: ${MATTERMOST_POSTGRES_MEMORY_LIMIT:-1g} cpus: "${MATTERMOST_POSTGRES_CPU_LIMIT:-1.0}" reservations: memory: ${MATTERMOST_POSTGRES_MEMORY_RESERVATION:-128m} cpus: "${MATTERMOST_POSTGRES_CPU_RESERVATION:-0.25}" volumes: - mattermost-postgres:/var/lib/postgresql/data environment: POSTGRES_DB: ${MATTERMOST_DB_NAME:-mattermostdb} POSTGRES_USER: ${MATTERMOST_DB_USER:-mattermostdbuser} POSTGRES_PASSWORD: ${MATTERMOST_DB_PASSWORD:?set in .env - see .env.example for generation command} networks: - mattermost-network healthcheck: test: [ "CMD", "pg_isready", "-q", "-d", "${MATTERMOST_DB_NAME:-mattermostdb}", "-U", "${MATTERMOST_DB_USER:-mattermostdbuser}" ] interval: 10s timeout: 5s retries: 3 start_period: 60s restart: unless-stopped mattermost: image: *mattermost-image # Resource limits as .env-overridable defaults (e.g. MATTERMOST_MEMORY_LIMIT=2g); # the override survives every git pull. deploy: resources: limits: memory: ${MATTERMOST_MEMORY_LIMIT:-1g} cpus: "${MATTERMOST_CPU_LIMIT:-1.0}" pids: 200 reservations: memory: ${MATTERMOST_MEMORY_RESERVATION:-128m} cpus: "${MATTERMOST_CPU_RESERVATION:-0.25}" volumes: - mattermost-data:${DATA_PATH:-/mattermost/data} - mattermost-config:/mattermost/config - mattermost-logs:/mattermost/logs - mattermost-plugins:/mattermost/plugins - mattermost-client-plugins:/mattermost/client/plugins - mattermost-bleve-indexes:/mattermost/bleve-indexes environment: TZ: ${MATTERMOST_TIMEZONE:-UTC} MM_SQLSETTINGS_DRIVERNAME: ${MATTERMOST_DB_TYPE:-postgres} MM_SQLSETTINGS_DATASOURCE: postgres://${MATTERMOST_DB_USER:-mattermostdbuser}:${MATTERMOST_DB_PASSWORD}@postgres:5432/${MATTERMOST_DB_NAME:-mattermostdb}?sslmode=disable&connect_timeout=10 MM_BLEVESETTINGS_INDEXDIR: /mattermost/bleve-indexes DOMAIN: ${MATTERMOST_HOSTNAME:?set in .env - see .env.example} MM_SERVICESETTINGS_SITEURL: ${MATTERMOST_URL:?set in .env - see .env.example} MM_SERVICESETTINGS_FORWARD80TO443: 'false' MATTERMOST_CONTAINER_READONLY: 'false' networks: - mattermost-network - traefik-network security_opt: - no-new-privileges:true read_only: 'false' tmpfs: - /tmp labels: # Enable Traefik for this container - "traefik.enable=true" # Match incoming requests on a specific hostname - "traefik.http.routers.mattermost.rule=Host(`${MATTERMOST_HOSTNAME}`)" # Assign the router to a named Traefik service - "traefik.http.routers.mattermost.service=mattermost" # Use the 'websecure' (HTTPS) entry point - "traefik.http.routers.mattermost.entrypoints=websecure" # Define the internal container port for routing - "traefik.http.services.mattermost.loadbalancer.server.port=8065" # Enable TLS on this router - "traefik.http.routers.mattermost.tls=true" # Use Let's Encrypt for certificate management - "traefik.http.routers.mattermost.tls.certresolver=letsencrypt" # Pass the original Host header to the container - "traefik.http.services.mattermost.loadbalancer.passhostheader=true" # Apply a compression middleware - "traefik.http.routers.mattermost.middlewares=compresstraefik" # Define settings for the compression middleware - "traefik.http.middlewares.compresstraefik.compress=true" # Assign the Zabbix UDP router to a named Traefik service - "traefik.udp.routers.mm-call-rtr.service=mm-call-svc" # Use the 'zabbix-udp' (custom) entry point - "traefik.udp.routers.mm-call-rtr.entrypoints=calls" # Define the internal container port for routing to the Zabbix UDP service - "traefik.udp.services.mm-call-svc.loadBalancer.server.port=8443" # Specify which Docker network Traefik should use for routing - "traefik.docker.network=traefik-network" restart: unless-stopped depends_on: postgres: condition: service_healthy traefik: condition: service_healthy traefik: image: *traefik-image # Hardening: no privilege escalation via setuid binaries security_opt: - no-new-privileges:true # Drop every Linux capability; add back only what this image's # entrypoint needs (bind :80/:443, chown its data directory, drop to # its service user). cap_drop: - ALL cap_add: - NET_BIND_SERVICE # Resource limits as .env-overridable defaults (e.g. TRAEFIK_MEMORY_LIMIT=2g); # the override survives every git pull. deploy: resources: limits: memory: ${TRAEFIK_MEMORY_LIMIT:-256m} cpus: "${TRAEFIK_CPU_LIMIT:-0.5}" reservations: memory: ${TRAEFIK_MEMORY_RESERVATION:-64m} cpus: "${TRAEFIK_CPU_RESERVATION:-0.1}" command: # Set the log level (DEBUG, INFO, WARN, ERROR) - "--log.level=${TRAEFIK_LOG_LEVEL:-WARN}" # Enable the built-in API and web-based dashboard - "--api.dashboard=true" # Enable the /ping endpoint so we can health-check Traefik - "--ping=true" # Assign the /ping endpoint to a dedicated entry point on port 8082 - "--ping.entrypoint=ping" - "--entrypoints.ping.address=:8082" # Define the primary HTTP entry point on port 80 - "--entrypoints.web.address=:80" # Define the secure (HTTPS) entry point on port 443 - "--entrypoints.websecure.address=:443" # Timeouts on the HTTPS entry point. Unset, they are Traefik's own # defaults; set them in .env to tune (see .env.example). - "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=${TRAEFIK_READ_TIMEOUT:-60s}" - "--entrypoints.websecure.transport.respondingTimeouts.writeTimeout=${TRAEFIK_WRITE_TIMEOUT:-0s}" - "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=${TRAEFIK_IDLE_TIMEOUT:-180s}" # HTTP -> HTTPS redirect at entrypoint level - "--entrypoints.web.http.redirections.entrypoint.to=websecure" - "--entrypoints.web.http.redirections.entrypoint.scheme=https" # Define the Mattermost UDP entry point on port 8443 - "--entryPoints.calls.address=:8443/udp" # Enable the Docker provider to detect containers and their labels - "--providers.docker=true" # Point Traefik to the Docker socket - "--providers.docker.endpoint=unix:///var/run/docker.sock" # Prevent automatic exposure of all containers; only expose containers # with "traefik.enable=true" - "--providers.docker.exposedbydefault=false" # Use ACME (Let's Encrypt) to generate/renew certificates via TLS challenge - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" # The email address used by Let's Encrypt for renewal notices - "--certificatesresolvers.letsencrypt.acme.email=${TRAEFIK_ACME_EMAIL:?set in .env - see .env.example}" # The file where ACME certificates are stored inside the container - "--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json" # Enable Prometheus metrics - "--metrics.prometheus=true" # Configure Prometheus histogram buckets - "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0" # Check for newer Traefik versions and optionally log that info - "--global.checknewversion=true" # Disable sending anonymous usage data to the Traefik maintainers - "--global.sendanonymoususage=false" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - traefik-certificates:/etc/traefik/acme networks: - traefik-network ports: - "80:80" - "443:443" - "8443:8443/udp" healthcheck: test: ["CMD", "wget", "http://localhost:8082/ping","--spider"] interval: 10s timeout: 5s retries: 3 start_period: 5s labels: # Enable Traefik for this container - "traefik.enable=true" # A router to expose the Traefik dashboard - "traefik.http.routers.dashboard.rule=Host(`${TRAEFIK_HOSTNAME:?set in .env - see .env.example}`)" - "traefik.http.routers.dashboard.entrypoints=websecure" - "traefik.http.routers.dashboard.tls=true" - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt" - "traefik.http.routers.dashboard.service=api@internal" # Basic Authentication for the Traefik dashboard - "traefik.http.routers.dashboard.middlewares=authtraefik" - "traefik.http.middlewares.authtraefik.basicauth.users=${TRAEFIK_BASIC_AUTH:?set in .env - see .env.example for htpasswd generation}" # Specify the internal server port to the dashboard service - "traefik.http.services.dashboard.loadbalancer.server.port=8080" # Pass the original Host header to the backend - "traefik.http.services.dashboard.loadbalancer.passhostheader=true" restart: unless-stopped backups: image: *postgres-image # Hardening: no privilege escalation via setuid binaries security_opt: - no-new-privileges:true # Drop every Linux capability; add back only what this image's # entrypoint needs (bind :80/:443, chown its data directory, drop to # its service user). cap_drop: - ALL cap_add: - CHOWN - DAC_OVERRIDE - FOWNER # Resource limits as .env-overridable defaults (e.g. MATTERMOST_BACKUPS_MEMORY_LIMIT=2g); # the override survives every git pull. deploy: resources: limits: memory: ${MATTERMOST_BACKUPS_MEMORY_LIMIT:-256m} cpus: "${MATTERMOST_BACKUPS_CPU_LIMIT:-0.5}" reservations: memory: ${MATTERMOST_BACKUPS_MEMORY_RESERVATION:-32m} cpus: "${MATTERMOST_BACKUPS_CPU_RESERVATION:-0.1}" command: >- bash -c 'set -o pipefail; sleep "$$BACKUP_INIT_SLEEP"; while true; do STAMP="$$(date +%Y-%m-%d_%H-%M)"; DB_FILE="$$POSTGRES_BACKUPS_PATH/$$POSTGRES_BACKUP_NAME-$$STAMP.gz"; echo "[$$(date -Iseconds)] Starting database backup to $$DB_FILE"; if pg_dump -h postgres -p 5432 -d $$MATTERMOST_DB_NAME -U $$MATTERMOST_DB_USER | gzip > "$$DB_FILE.partial"; then mv "$$DB_FILE.partial" "$$DB_FILE"; echo "[$$(date -Iseconds)] Database backup OK: $$DB_FILE ($$(stat -c %s "$$DB_FILE") bytes)"; else echo "[$$(date -Iseconds)] Database backup FAILED - partial file kept as $$DB_FILE.failed for diagnosis" >&2; mv "$$DB_FILE.partial" "$$DB_FILE.failed" 2>/dev/null || true; fi; DATA_FILE="$$DATA_BACKUPS_PATH/$$DATA_BACKUP_NAME-$$STAMP.tar.gz"; echo "[$$(date -Iseconds)] Starting data backup to $$DATA_FILE"; # GNU tar exits 1 when a live application changed a file mid-read; # the archive is complete - only exit 2 is a real failure. But an exit # code has never been a statement about whether the file it produced # opens, so the archive is read back before it is renamed into place. # One tar -tzf a day is not a cost worth arguing about, and it is the # difference between a backup and a file named like one. No GNU-only # flags: alpine images ship BusyBox tar. tar -zcpf "$$DATA_FILE.partial" $$DATA_PATH; RC=$$?; if { [ "$$RC" -eq 0 ] || [ "$$RC" -eq 1 ]; } && tar -tzf "$$DATA_FILE.partial" > /dev/null 2>&1; then mv "$$DATA_FILE.partial" "$$DATA_FILE"; echo "[$$(date -Iseconds)] Data backup OK: $$DATA_FILE ($$(stat -c %s "$$DATA_FILE") bytes)$$([ "$$RC" -eq 1 ] && echo " - some files changed while being read")"; else echo "[$$(date -Iseconds)] Data backup FAILED - partial file kept as $$DATA_FILE.failed for diagnosis" >&2; mv "$$DATA_FILE.partial" "$$DATA_FILE.failed" 2>/dev/null || true; fi; if [ "$${POSTGRES_BACKUP_PRUNE_DAYS:-0}" -ge 1 ]; then find "$$POSTGRES_BACKUPS_PATH" -type f -name "$$POSTGRES_BACKUP_NAME-*" -mtime "+$$POSTGRES_BACKUP_PRUNE_DAYS" -delete; fi; if [ "$${DATA_BACKUP_PRUNE_DAYS:-0}" -ge 1 ]; then find "$$DATA_BACKUPS_PATH" -type f -name "$$DATA_BACKUP_NAME-*" -mtime "+$$DATA_BACKUP_PRUNE_DAYS" -delete; fi; sleep "$$BACKUP_INTERVAL"; done' volumes: - mattermost-postgres-backup:/var/lib/postgresql/data - mattermost-data:${DATA_PATH:-/mattermost/data} - mattermost-data-backups:${DATA_BACKUPS_PATH:-/srv/mattermost-application-data/backups} - mattermost-database-backups:${POSTGRES_BACKUPS_PATH:-/srv/mattermost-postgres/backups} environment: MATTERMOST_DB_NAME: ${MATTERMOST_DB_NAME:-mattermostdb} MATTERMOST_DB_USER: ${MATTERMOST_DB_USER:-mattermostdbuser} PGPASSWORD: ${MATTERMOST_DB_PASSWORD} BACKUP_INIT_SLEEP: ${BACKUP_INIT_SLEEP:-30m} BACKUP_INTERVAL: ${BACKUP_INTERVAL:-24h} POSTGRES_BACKUP_PRUNE_DAYS: ${POSTGRES_BACKUP_PRUNE_DAYS:-7} DATA_BACKUP_PRUNE_DAYS: ${DATA_BACKUP_PRUNE_DAYS:-7} POSTGRES_BACKUPS_PATH: ${POSTGRES_BACKUPS_PATH:-/srv/mattermost-postgres/backups} DATA_BACKUPS_PATH: ${DATA_BACKUPS_PATH:-/srv/mattermost-application-data/backups} DATA_PATH: ${DATA_PATH:-/mattermost/data} POSTGRES_BACKUP_NAME: ${POSTGRES_BACKUP_NAME:-mattermost-postgres-backup} DATA_BACKUP_NAME: ${DATA_BACKUP_NAME:-mattermost-application-data-backup} networks: - mattermost-network restart: unless-stopped depends_on: postgres: condition: service_healthy