Teams often hit a wall when internal chat becomes a patchwork of consumer apps, unmanaged bots, and unclear retention policies. If you need searchable channels, incident rooms, SSO-ready identity controls, and data ownership under your own domain, Mattermost is a practical self-hosted option. This guide walks through a production deployment of Mattermost on Ubuntu using Docker Compose, Caddy for automatic HTTPS, and PostgreSQL as the persistent backend. The focus is operational reliability: repeatable deploy steps, secret handling, safe upgrades, backups, and real-world troubleshooting.
Architecture and flow overview
This stack separates concerns so each component has a clear operational role. Caddy terminates TLS and forwards traffic to Mattermost. Mattermost runs as an application container and stores durable state in PostgreSQL and local file storage for uploads. Docker Compose orchestrates lifecycle and networking.
- Caddy: reverse proxy, HTTPS certificate automation, HTTP to HTTPS redirect, security headers.
- Mattermost: team chat app, API, websocket traffic, file and plugin handling.
- PostgreSQL: primary relational datastore (users, channels, messages metadata, config state).
- Volumes: persistent storage for Postgres data and Mattermost application data.
Request path: user browser -> chat.example.com -> Caddy (TLS) -> Mattermost container -> PostgreSQL. Keep DNS, TLS, and database persistence healthy and the platform remains stable through restarts and upgrades.
Prerequisites
- Ubuntu 22.04/24.04 server with 2+ vCPU and 4+ GB RAM (8 GB recommended for active teams).
- A domain/subdomain pointed to your server, for example
chat.example.com. - Open inbound ports
80and443in cloud/network firewall. - A non-root sudo user.
- Docker Engine + Docker Compose plugin installed.
sudo apt update
sudo apt install -y ca-certificates curl gnupg lsb-release
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
If the copy button does not work in your browser/session, manually select the code block and copy it.
Step-by-step deployment
1) Prepare project directories and environment file
Create an isolated project path and define secrets in an environment file. Never hardcode secrets in version-controlled compose files.
mkdir -p ~/mattermost-stack/{caddy,data/postgres,data/mattermost}
cd ~/mattermost-stack
cat > .env <<'EOF'
DOMAIN=chat.example.com
MM_DB_NAME=mattermost
MM_DB_USER=mmuser
MM_DB_PASSWORD=replace_with_long_random_password
POSTGRES_PASSWORD=replace_with_another_long_random_password
MM_SITE_URL=https://chat.example.com
TZ=UTC
EOF
chmod 600 .env
If the copy button does not work in your browser/session, manually select the code block and copy it.
2) Create Docker Compose file
This compose definition pins services, wires internal networking, and maps only required host ports through Caddy.
services:
postgres:
image: postgres:16
container_name: mm-postgres
restart: unless-stopped
env_file: .env
environment:
POSTGRES_DB: ${MM_DB_NAME}
POSTGRES_USER: ${MM_DB_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: ${TZ}
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${MM_DB_USER} -d ${MM_DB_NAME}"]
interval: 10s
timeout: 5s
retries: 10
mattermost:
image: mattermost/mattermost-team-edition:latest
container_name: mattermost
restart: unless-stopped
env_file: .env
depends_on:
postgres:
condition: service_healthy
environment:
MM_SQLSETTINGS_DRIVERNAME: postgres
MM_SQLSETTINGS_DATASOURCE: postgres://${MM_DB_USER}:${MM_DB_PASSWORD}@postgres:5432/${MM_DB_NAME}?sslmode=disable&connect_timeout=10
MM_SERVICESETTINGS_SITEURL: ${MM_SITE_URL}
MM_SERVICESETTINGS_ENABLELOCALMODE: "false"
MM_LOGSETTINGS_CONSOLELEVEL: INFO
MM_PLUGINSETTINGS_ENABLE: "true"
TZ: ${TZ}
volumes:
- ./data/mattermost/config:/mattermost/config
- ./data/mattermost/data:/mattermost/data
- ./data/mattermost/logs:/mattermost/logs
- ./data/mattermost/plugins:/mattermost/plugins
- ./data/mattermost/client-plugins:/mattermost/client/plugins
- ./data/mattermost/bleve-indexes:/mattermost/bleve-indexes
caddy:
image: caddy:2
container_name: mm-caddy
restart: unless-stopped
env_file: .env
depends_on:
- mattermost
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./data/caddy:/data
- ./config/caddy:/config
networks:
default:
name: mattermost-net
If the copy button does not work in your browser/session, manually select the code block and copy it.
3) Configure Caddy reverse proxy
Set websocket-safe reverse proxying and basic hardening headers.
cat > caddy/Caddyfile <<'EOF'
{$DOMAIN} {
encode zstd gzip
@websockets {
header Connection *Upgrade*
header Upgrade websocket
}
reverse_proxy mattermost:8065
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
EOF
If the copy button does not work in your browser/session, manually select the code block and copy it.
4) Launch stack and inspect health
docker compose pull
docker compose up -d
docker compose ps
docker compose logs -f --tail=100 mattermost
If the copy button does not work in your browser/session, manually select the code block and copy it.
When the stack is healthy, open https://chat.example.com and complete initial admin onboarding. Immediately enforce MFA/SSO policy and disable anonymous exposure if this is an internal deployment.
5) Configure SMTP and production app settings
Mail delivery is essential for invites, password resets, and notifications. Configure through system console or environment variables and verify outbound relay policies (SPF/DKIM/DMARC alignment where applicable).
# Example: inspect effective runtime config file location
docker exec -it mattermost sh -lc 'ls -lah /mattermost/config && grep -n "ServiceSettings" -n /mattermost/config/config.json | head'
If the copy button does not work in your browser/session, manually select the code block and copy it.
6) Backups and upgrade workflow
Production readiness depends less on first deploy and more on safe change operations. Before upgrades, snapshot both PostgreSQL and Mattermost volumes. Use maintenance windows for major version jumps and always test restore.
# PostgreSQL logical backup
mkdir -p ~/backups/mattermost
cd ~/mattermost-stack
docker exec -t mm-postgres pg_dump -U "$MM_DB_USER" "$MM_DB_NAME" \
> ~/backups/mattermost/mm_$(date +%F_%H%M).sql
# App data backup
tar -czf ~/backups/mattermost/mm_data_$(date +%F_%H%M).tar.gz -C ~/mattermost-stack/data mattermost
If the copy button does not work in your browser/session, manually select the code block and copy it.
Configuration and secrets handling best practices
Use unique, high-entropy passwords for database and admin accounts. Store long-term secrets in a secrets manager or encrypted vault rather than shell history. Keep .env permissioned to owner-only. If your team uses CI/CD, inject secrets at deploy time and avoid writing them into pipeline logs.
Enforce least privilege for database access: one app user and no superuser for runtime operations. Restrict host-level access with firewall policy (SSH from trusted IPs, public access only on 80/443). Add fail2ban or equivalent intrusion controls where possible.
For compliance-sensitive environments, configure message retention policies, export controls, and audit logging early. If integrating with SSO (SAML/OIDC), require central identity policy including MFA and access revocation workflows for offboarding.
Verification checklist
- DNS resolves correctly to the server IP.
- Valid HTTPS certificate is issued and auto-renewable.
- Admin account created, login successful, websocket updates live.
- PostgreSQL healthcheck passes and query latency is stable.
- File uploads/downloads work for expected size limits.
- Email invite and password reset emails deliver successfully.
- Backup and restore test completed in staging (not just backup creation).
# Quick smoke checks
curl -I https://chat.example.com
docker compose ps
docker exec -it mm-postgres psql -U "$MM_DB_USER" -d "$MM_DB_NAME" -c 'select now();'
If the copy button does not work in your browser/session, manually select the code block and copy it.
Common issues and fixes
TLS certificate not issued
Usually DNS mismatch or blocked port 80. Confirm A/AAAA records and cloud firewall settings. Caddy must be reachable for ACME HTTP challenge unless using DNS challenge.
Login works but channel updates lag or fail
Check reverse-proxy websocket handling and container logs. Ensure no intermediate load balancer strips Upgrade headers.
Database connection errors after restart
Validate credentials in .env and ensure startup order waits for PostgreSQL health. Corrupted or permission-broken data directories are also common after manual host changes.
Upload failures for larger files
Review Mattermost file settings and reverse proxy limits. If your organization exchanges large artifacts, set policy limits intentionally and document them for users.
High CPU during indexing/search
Monitor indexing workloads and schedule heavy operations off-peak. For larger teams, scale vertically and tune database resources before user experience degrades.
Upgrade introduced plugin incompatibility
Pin plugin versions and test upgrades in staging. Roll forward only after confirming app/plugin compatibility matrix and restore point availability.
FAQ
Can I run Mattermost without PostgreSQL?
PostgreSQL is the recommended production database. Running without it is not suitable for reliable team operations.
Do I need Caddy if I already have another load balancer?
No, but you still need a TLS-terminating reverse proxy that correctly handles websocket traffic and security headers.
How often should I back up data?
At minimum daily for moderate usage, plus pre-upgrade backups. Mission-critical teams usually do more frequent database backups with retention rotation.
What is the safest way to upgrade?
Pull new images in staging first, run smoke tests, take fresh backups, then upgrade production during a maintenance window with rollback prepared.
How do I secure admin access?
Enable MFA/SSO, limit admin role assignment, enforce strong passwords, and audit admin actions regularly.
Can this stack scale for larger organizations?
Yes, but plan capacity: stronger database host, monitored storage IOPS, and potentially externalized services as usage grows.
How should I handle secrets in automation pipelines?
Use secret stores (Vault, cloud secret managers), inject at runtime, and ensure CI logs never print secret values.
Related internal guides
- How to Deploy Gitea with Docker Compose and Caddy
- Deploy Metabase with Docker Compose + Nginx + PostgreSQL
- Deploy Harbor with Kubernetes + Helm + cert-manager
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.