The Problem: Your Team Needs Chat You Actually Control
Customers ask us a version of this every week: how do you give a growing team modern chat, file sharing, searchable conversations, and mobile access without handing every internal discussion to a third-party SaaS vendor?
For some teams, a hosted chat service is fine. But once you start thinking about Canadian data residency, retention policy, access control, or simply the risk of putting customer operations inside yet another external platform, self-hosting becomes much more attractive. Mattermost is one of the strongest options in that category. It gives you channels, direct messages, threads, file sharing, notifications, and a mature admin console, while letting you choose where the system runs and how it is backed up.
If you are still deciding between platforms, start with our draft comparison Mattermost vs Rocket.Chat: Which Team Chat Wins?. This guide assumes you have already chosen Mattermost and want a practical way to deploy it on infrastructure you control.
In this tutorial, we will deploy Mattermost with Docker Compose and PostgreSQL on a Canadian Web Hosting Cloud VPS, put it behind HTTPS, and cover the production basics that matter before you invite your team.
What You Will Need / Prerequisites
For a small production pilot, we recommend a Canadian Web Hosting Cloud VPS with at least 4 vCPUs, 8 GB RAM, and 80 GB of SSD storage. That gives you headroom for the Mattermost application container, PostgreSQL, logs, uploads, and a reverse proxy. If you expect larger file uploads, long retention, or heavier concurrency, size up early rather than trying to squeeze chat, search, and database activity into an undersized VM.
Mattermost’s official Docker deployment documentation requires Linux for supported production deployments, along with Docker Engine and Docker Compose. The current official example environment file also pins concrete image tags rather than floating versions, which is a good habit for production because it makes updates deliberate and reproducible.
- A domain name such as
chat.example.ca - Ubuntu 24.04 LTS on your VPS
- Docker Engine and the Docker Compose plugin installed
- DNS pointed at your VPS
- Ports
80and443open for HTTPS - Sudo access on the server
If you prefer a fully managed rollout, add Managed Support so our team can handle deployment, patching, and ongoing maintenance.
Installation Steps
1. Prepare the server and install Docker
Start with a clean Ubuntu host, update packages, and install Docker Engine plus the Compose plugin. This gives you the runtime Mattermost’s official container deployment expects.
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg ufw
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
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
Verify the install before you keep going:
docker --version
docker compose version
sudo systemctl status docker --no-pager
You should see Docker running and both version commands returning successfully. If the daemon is inactive, do not continue until systemctl status docker shows it is up.
2. Clone the official Mattermost Docker deployment
Mattermost maintains an official Docker deployment repository. Cloning it gives you the supported structure, environment file, and service definitions that the documentation expects.
git clone https://github.com/mattermost/docker.git
cd docker
cp env.example .env
At this stage, open the environment file and change the domain plus any credentials you do not want to leave at defaults:
nano .env
At minimum, update these values:
DOMAIN=chat.example.ca
POSTGRES_USER=mmuser
POSTGRES_PASSWORD=change-this-to-a-long-random-password
POSTGRES_DB=mattermost
MATTERMOST_IMAGE_TAG=10.11.8
POSTGRES_IMAGE_TAG=18-alpine
The version examples above come from Mattermost’s current official Docker example. Before you deploy, re-check the upstream repository and pin the specific version you want rather than using a generic moving tag.
3. Create persistent storage with the correct ownership
Mattermost uses multiple directories for configuration, uploads, logs, plugins, and search indexes. The official docs also call out the container ownership requirement. If this step is skipped, the app may fail to start cleanly or may write logs but not uploads.
mkdir -p ./volumes/app/mattermost/{config,data,logs,plugins,client/plugins,bleve-indexes}
sudo chown -R 2000:2000 ./volumes/app/mattermost
Expected result: the directories exist and are writable by the Mattermost container user. Confirm with:
ls -ld ./volumes/app/mattermost ./volumes/app/mattermost/data
4. Launch Mattermost and PostgreSQL
For a clean first deployment, bring up the core application and database first. This tutorial uses the official “without nginx” override so Mattermost listens on port 8065 internally and can sit behind your preferred reverse proxy.
docker compose -f docker-compose.yml -f docker-compose.without-nginx.yml up -d
Check the containers immediately:
docker compose ps
docker compose logs mattermost --tail=50
docker compose logs postgres --tail=50
You want both services in an Up state. The Mattermost logs should move past database connection attempts and into normal application startup. If PostgreSQL keeps restarting, double-check the database credentials in .env.
5. Put Mattermost behind HTTPS
Running chat over plain HTTP is not acceptable for production. The fastest path is to place Mattermost behind a reverse proxy that handles TLS and forwards traffic to port 8065. If you already standardise on Caddy, our published guide on Caddy with automatic HTTPS is a strong fit for Mattermost as well.
A minimal Caddy site block looks like this:
chat.example.ca {
reverse_proxy 127.0.0.1:8065
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
}
}
After reloading Caddy, update your Mattermost site URL to match the HTTPS endpoint if you changed it after first boot.
Configuration
The official Docker deployment uses environment variables for the important first-run settings. Here is the section you should understand before putting users on the system:
DOMAIN=chat.example.ca
TZ=UTC
POSTGRES_USER=mmuser
POSTGRES_PASSWORD=use-a-long-random-secret
POSTGRES_DB=mattermost
POSTGRES_IMAGE_TAG=18-alpine
MATTERMOST_IMAGE=mattermost-enterprise-edition
MATTERMOST_IMAGE_TAG=10.11.8
APP_PORT=8065
MM_SQLSETTINGS_DRIVERNAME=postgres
MM_SQLSETTINGS_DATASOURCE=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable&connect_timeout=10
MM_SERVICESETTINGS_SITEURL=https://${DOMAIN}
- DOMAIN tells the stack what hostname users will visit.
- TZ keeps logs and scheduled behaviour consistent.
- POSTGRES_* defines the database credentials and image tag.
- MATTERMOST_IMAGE_TAG pins your application version so upgrades are intentional.
- MM_SQLSETTINGS_DATASOURCE wires the app to PostgreSQL.
- MM_SERVICESETTINGS_SITEURL must match your public HTTPS URL, or notifications and links can behave badly.
For a small internal deployment, keep the configuration simple at first: local PostgreSQL, local uploads, a single app node, and a reverse proxy handling TLS. Add SSO, external object storage, or a more complex topology only after the base platform is stable.
Verify It Works
Once the stack is running and your reverse proxy is in front, complete three checks before inviting users:
- HTTP health:
curl -I http://127.0.0.1:8065should return an HTTP response from Mattermost. - External HTTPS: browse to
https://chat.example.caand complete the first admin setup. - Container health:
docker compose psshould show stable containers with no restart loop.
You should also test a real user path: create a channel, upload a file, send a direct message, and sign in from mobile or desktop. A chat platform that “loads in the browser” is not the same thing as one that is ready for a team rollout.
Production Hardening
Lock down the firewall
Do not leave unnecessary ports exposed. If your reverse proxy runs on the host, keep only SSH, HTTP, and HTTPS open publicly.
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
If you are proxying Mattermost locally, you do not need to expose 8065 to the internet.
Use HTTPS everywhere
Set MM_SERVICESETTINGS_SITEURL to the final https:// URL and confirm TLS is terminating correctly at the reverse proxy. This protects login sessions, mobile connections, and file transfers.
Back up data before you need it
Your backup plan should include the PostgreSQL data directory plus the Mattermost volumes that store uploads, configuration, logs, plugins, and indexes. If chat history matters to the business, test restore steps as well as backup creation. Pair the deployment with CloudSafe Backup or your preferred off-server backup workflow so a single host failure does not take your collaboration history with it.
Plan log retention
Mattermost writes logs quickly on active teams. Make sure Docker logs and the application log directory are not allowed to grow without limits. If you want central visibility across multiple servers, add a logging stack later rather than waiting for a storage emergency.
Troubleshooting
Container exits immediately
Cause: permissions on the Mattermost volumes are wrong, or the database variables are mismatched.
Check:
docker compose logs mattermost --tail=100
ls -ld ./volumes/app/mattermost ./volumes/app/mattermost/data
Fix:
sudo chown -R 2000:2000 ./volumes/app/mattermost
docker compose down
docker compose -f docker-compose.yml -f docker-compose.without-nginx.yml up -d
Verify: the Mattermost container stays up and docker compose ps no longer shows restarts.
Browser loads, but login links or notifications point to the wrong URL
Cause: the site URL is still set to HTTP or to the wrong hostname.
Check: inspect MM_SERVICESETTINGS_SITEURL in your .env file.
Fix:
nano .env
# set MM_SERVICESETTINGS_SITEURL=https://chat.example.ca
docker compose -f docker-compose.yml -f docker-compose.without-nginx.yml up -d
Verify: invitation emails, links, and redirects all use the public HTTPS URL.
Uploads or search feel slow after rollout
Cause: the server is undersized, storage is too tight, or you need to revisit retention and attachment policy.
Check:
docker stats --no-stream
df -h
free -h
Fix: increase VPS resources, move to faster storage, or adjust file retention before users treat the system like a long-term archive.
Verify: uploads complete quickly, search indexes build without exhausting RAM, and the host is no longer swapping under normal use.
Conclusion / Next Steps
Mattermost is a strong fit when your team wants modern collaboration without giving up infrastructure control. The official Docker deployment gives you a practical way to get running quickly, and a Canadian Web Hosting Cloud VPS gives you the flexibility to keep the platform in Canada, size it for your team, and grow it as usage increases.
If you are still validating whether Mattermost is the right platform, compare it with Rocket.Chat in our draft Mattermost vs Rocket.Chat. Once your pilot is live, the next jobs are predictable: connect identity, test backups, monitor storage growth, and document your upgrade routine. If you want help getting that production layer right, talk to us about Managed Support or start with a Cloud VPS sized for your team.
If you want to keep admin access to this stack off the public internet, pair it with our draft WireGuard VPN guide for secure remote admin access. It shows how to move SSH behind a private tunnel before you treat the server like normal internet-facing infrastructure.
Most teams do not stop at chat alone. If you also need a private documentation hub for SOPs, onboarding, and internal runbooks, our comparison of BookStack vs Wiki.js is the logical next step.
Be First to Comment