Slack built its reputation on the idea that email is where conversations go to die. Fair point. But handing your entire team’s communication history to a third-party platform creates its own set of headaches: unpredictable pricing tiers, vendor lock-in, and data sovereignty concerns that keep compliance teams up at night.
Mattermost is the antidote. It’s an open-source messaging platform that delivers everything Slack offers — persistent channels, direct messages, threaded replies, file sharing, and integrations — while keeping your data firmly on your own infrastructure. Nobody reads your messages. Nobody mines your team’s conversations. You own it all.
This guide walks you through installing Mattermost on a VPS using Docker and Docker Compose, with Nginx as a reverse proxy and Let’s Encrypt SSL for secure HTTPS access. By the end you’ll have a production-ready team messaging platform that’s ready to onboard your first users.
What Is Mattermost?
Originally launched in 2015, Mattermost started as an internal tool for a gaming company before its founders decided to release it as open source. It has since become one of the most widely deployed self-hosted messaging platforms in the world, especially popular among organizations in regulated industries like healthcare, finance, and defense where data residency isn’t optional.
The free Team Edition covers everything most organizations actually need: unlimited message history, channels, direct messages, full-text search, file sharing, webhooks, and a growing library of integrations. The paid Enterprise Edition layers on LDAP/SAML authentication, compliance exports, and advanced reporting — valuable at scale but not required to get started.
Under the hood, Mattermost runs as a single Go binary connected to a PostgreSQL database. Docker packages that binary along with all its dependencies into containers, making deployment predictable and repeatable regardless of what else runs on your server.
Prerequisites
Before starting, verify that your VPS meets these requirements:
| Requirement | Minimum | Recommended |
|---|---|---|
| Operating System | Ubuntu 22.04 LTS | Ubuntu 24.04 LTS |
| CPU | 2 cores | 4 cores |
| RAM | 4 GB | 8 GB |
| Storage | 20 GB SSD | 50 GB SSD |
| Network | Domain A record pointing to server IP | Dedicated subdomain (e.g., chat.yourdomain.com) |
You’ll also need root or sudo access to the server. Create your DNS A record before starting — Certbot must be able to resolve your domain during certificate issuance and DNS propagation can take up to 24 hours.
How to Install Mattermost on a VPS
Step 1: Update Your Server
Connect to your VPS via SSH and apply all pending updates before anything else:
sudo apt-get update && sudo apt-get upgrade -y
If the kernel was updated, reboot to make sure you’re running the freshest possible base:
sudo reboot
Step 2: Install Docker Engine
Ubuntu’s default package manager ships an outdated Docker build. Always install Docker Engine directly from the official Docker repository instead:
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
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-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Add your user to the docker group so you can run Docker commands without sudo:
sudo usermod -aG docker $USER
newgrp docker
Confirm both Docker and Docker Compose installed correctly:
docker --version
docker compose version
Step 3: Configure the UFW Firewall
Lock down the server to only the connections you actually need:
sudo ufw allow OpenSSH
sudo ufw allow "Nginx Full"
sudo ufw enable
Verify the active rules:
sudo ufw status
You should see SSH (22), HTTP (80), and HTTPS (443) listed as allowed. Mattermost itself listens on port 8065 inside Docker but binds only to 127.0.0.1, so it never needs a direct firewall rule — Nginx handles all public traffic on its behalf.
Step 4: Create the Project Directory
Keep all deployment files organized under a dedicated directory:
sudo mkdir -p /opt/mattermost
sudo chown $USER:$USER /opt/mattermost
cd /opt/mattermost
Step 5: Configure the Environment File
Store credentials in a .env file rather than hardcoding them directly into the Compose configuration. Create it now:
nano /opt/mattermost/.env
Paste the following and replace the placeholders with real values:
POSTGRES_USER=mattermost
POSTGRES_PASSWORD=change_this_to_a_strong_password
POSTGRES_DB=mattermost
SITE_URL=https://chat.yourdomain.com
Save and close the file (Ctrl+O, Enter, Ctrl+X), then restrict permissions so only your user can read it:
chmod 600 /opt/mattermost/.env
Docker Compose automatically reads a .env file from the project directory so no additional configuration is needed to wire it up.
Step 6: Create the Docker Compose File
Create the Compose file in the same directory:
nano /opt/mattermost/docker-compose.yml
Paste this configuration:
version: "3.9"
services:
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
mattermost:
image: mattermost/mattermost-team-edition:9
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
ports:
- "127.0.0.1:8065:8065"
environment:
MM_SQLSETTINGS_DRIVERNAME: postgres
MM_SQLSETTINGS_DATASOURCE: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable
MM_SERVICESETTINGS_SITEURL: ${SITE_URL}
MM_BLEVESETTINGS_INDEXDIR: /mattermost/bleve-indexes
volumes:
- mattermost-config:/mattermost/config
- mattermost-data:/mattermost/data
- mattermost-logs:/mattermost/logs
- mattermost-plugins:/mattermost/plugins
- mattermost-client-plugins:/mattermost/client/plugins
- mattermost-bleve:/mattermost/bleve-indexes
ulimits:
nofile:
soft: 49152
hard: 49152
volumes:
postgres-data:
mattermost-config:
mattermost-data:
mattermost-logs:
mattermost-plugins:
mattermost-client-plugins:
mattermost-bleve:
A few details worth understanding about this configuration:
- Healthcheck-based startup order: The
depends_oncondition usesservice_healthy, meaning Mattermost won’t start until PostgreSQL passes its health check. This eliminates the common first-boot race condition where Mattermost tries to reach a database that hasn’t finished initializing. - Loopback-only port binding: Port 8065 is bound to
127.0.0.1, not0.0.0.0. Mattermost is never directly reachable from the public internet — all traffic passes through Nginx. - File descriptor limits: The
ulimitsblock raises the open file limit to 49,152, per Mattermost’s production recommendation. This prevents WebSocket connections from hitting OS-level defaults under real load. - Verify the image tag: Before deploying, confirm that
:9is the current major version tag on Docker Hub.
Step 7: Start Mattermost
Launch the stack in detached mode:
docker compose up -d
PostgreSQL starts first and must pass its health check before Mattermost comes online. Check container status:
docker compose ps
Both services should show as running. Tail the Mattermost logs to confirm a clean start:
docker compose logs mattermost --follow
Look for a line that reads Server is listening on :8065. Press Ctrl+C to exit the log stream once you see it.
Step 8: Install and Configure Nginx
Install Nginx from the Ubuntu package manager:
sudo apt-get install -y nginx
Create a server block for Mattermost. Start with an HTTP-only configuration that Certbot can use for domain validation in the next step:
sudo nano /etc/nginx/sites-available/mattermost
Paste this initial configuration, replacing chat.yourdomain.com with your actual subdomain:
server {
listen 80;
server_name chat.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8065;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Enable the site, test the configuration and reload Nginx:
sudo ln -s /etc/nginx/sites-available/mattermost /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Step 9: Secure with Let’s Encrypt SSL
Install Certbot with the Nginx plugin:
sudo apt-get install -y certbot python3-certbot-nginx
Request a free SSL certificate for your subdomain:
sudo certbot --nginx -d chat.yourdomain.com
Certbot automatically modifies your Nginx configuration to add SSL. When prompted, choose to redirect all HTTP traffic to HTTPS — that’s option 2. Then verify that the automatic renewal timer is active:
sudo systemctl status certbot.timer
Step 10: Optimize Nginx for Mattermost
Certbot adds SSL support but Mattermost needs two things the default config doesn’t provide: WebSocket support for real-time messaging and a proxy cache to reduce backend load. Replace the contents of your Nginx config with this optimized version:
sudo nano /etc/nginx/sites-available/mattermost
Replace all existing content with the following, updating both chat.yourdomain.com references to match your actual domain:
upstream mattermost {
server 127.0.0.1:8065;
keepalive 32;
}
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=mattermost_cache:10m max_size=3g inactive=120m use_temp_path=off;
server {
listen 80;
server_name chat.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name chat.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/chat.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/chat.yourdomain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# WebSocket connections for real-time messaging
location ~ /api/v[0-9]+/(users/)?websocket$ {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
client_max_body_size 50M;
proxy_buffers 256 16k;
proxy_buffer_size 16k;
proxy_read_timeout 90s;
proxy_send_timeout 300s;
proxy_connect_timeout 90s;
proxy_pass http://mattermost;
}
# General application traffic
location / {
proxy_set_header Connection "";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
client_max_body_size 50M;
proxy_buffers 256 16k;
proxy_buffer_size 16k;
proxy_read_timeout 600s;
proxy_cache mattermost_cache;
proxy_cache_revalidate on;
proxy_cache_min_uses 2;
proxy_cache_use_stale timeout;
proxy_cache_lock on;
proxy_http_version 1.1;
proxy_pass http://mattermost;
}
}
Test and apply the updated configuration:
sudo nginx -t
sudo systemctl reload nginx
The dedicated WebSocket location block uses a regex that matches Mattermost’s real-time API paths and applies the Upgrade and Connection headers required to keep persistent connections alive. Without it, messages only appear after a manual page refresh — not ideal for a team chat platform.
Step 11: Complete the Setup Wizard
Open a browser and navigate to https://chat.yourdomain.com. Mattermost presents its first-run setup wizard:
- Create your administrator account. Use a strong, unique password and an email address you actually control.
- Name your organization and configure your first workspace.
- Invite team members or skip that step and revisit it from the admin panel later.
The first account created automatically becomes the system administrator. Save those credentials in a password manager before doing anything else — recovering a lost admin account without command-line access is a frustrating experience you want to avoid.
How to Update Mattermost
Mattermost releases updates frequently. Applying them with Docker is a two-command operation:
cd /opt/mattermost
docker compose pull
docker compose up -d
docker compose pull fetches the latest image matching your pinned tag. docker compose up -d recreates any containers whose image has changed. Mattermost handles database schema migrations automatically on startup after an update.
Before updating across a major version boundary, check the important upgrade notes in the Mattermost documentation for breaking changes or manual migration steps that might apply.
Troubleshooting
Mattermost Container Keeps Restarting
A restart loop almost always points to a failed database connection. Pull up the container logs immediately:
docker compose logs mattermost
Look for errors like pq: password authentication failed or dial tcp: connection refused. The first means the credentials don’t match. The second means PostgreSQL wasn’t fully ready when Mattermost tried to connect — rare given the healthcheck setup but possible on underpowered hardware. Confirm the values in your .env file are correct then restart the stack:
docker compose down
docker compose up -d
If the credentials look right, double-check the MM_SQLSETTINGS_DATASOURCE string in your Compose file. A single misplaced character in that connection string produces the exact same restart loop.
Messages Don’t Appear Without Refreshing the Page
Real-time messaging depends entirely on a persistent WebSocket connection. If new messages only show up after a manual page refresh, Nginx isn’t properly proxying the WebSocket endpoint. Confirm the WebSocket location block — with the Upgrade and Connection headers — exists in your Nginx config. Then reload after any changes:
sudo nginx -t && sudo systemctl reload nginx
You can verify the WebSocket connection directly in your browser. Open Developer Tools, go to the Network tab, filter by WS and reload the Mattermost page. An active WebSocket connection to your domain should appear immediately.
502 Bad Gateway on the Mattermost URL
A 502 error means Nginx is running but getting no response from Mattermost. Check whether the containers are actually up:
docker compose ps
If Mattermost shows unhealthy or exited, the container logs will explain why. Also confirm that the upstream block in your Nginx config points to 127.0.0.1:8065 and that the proxy_pass directives reference http://mattermost to match the upstream name. A port mismatch produces a 502 every time.
SSL Certificate Renewal Fails
Let’s Encrypt certificates expire after 90 days. Certbot’s systemd timer handles renewal automatically but can fail if port 80 is blocked or Nginx is down when the timer fires. Run a dry-run test to diagnose the issue without touching your live certificate:
sudo certbot renew --dry-run
If it fails, confirm that port 80 is open (sudo ufw status) and that Nginx is active (sudo systemctl status nginx). Certbot needs port 80 reachable from the internet to complete the HTTP-01 challenge during renewal.
File Uploads Fail Without an Error Message
Silent upload failures almost always trace back to a size limit mismatch between Nginx and Mattermost. Nginx’s client_max_body_size directive acts as a gate — if it’s set lower than Mattermost’s configured upload limit, large files fail at the Nginx layer before they ever reach the application and the user sees nothing useful.
The configuration in this guide sets client_max_body_size 50M in both location blocks. To raise the limit, update both the Nginx config and the MM_FILESETTINGS_MAXFILESIZE environment variable in your Compose file (value in bytes), then apply both changes:
docker compose up -d
sudo systemctl reload nginx
What to Build Next
A running Mattermost instance is the foundation. Here’s where to take it from here:
- Configure SMTP for email notifications: Without email, users can’t reset passwords or receive message digests. Add your SMTP credentials under System Console > Environment > SMTP to enable both.
- Connect your existing tools: Mattermost ships with native integrations for GitHub, GitLab, Jira, PagerDuty, and Zapier. Incoming webhooks let you push alerts from any system that can make an HTTP request.
- Enable Mattermost Calls: The Calls plugin adds voice and video conferencing directly inside channels. Smaller teams can use the built-in TURN server while larger deployments benefit from a dedicated RTCD instance.
- Explore Playbooks: Mattermost Playbooks turns incident response into a structured, repeatable workflow — define runbooks, assign tasks, and track resolution without ever leaving the chat interface.
- Set up database backups: All your data lives in Docker named volumes and the PostgreSQL container. Schedule regular
pg_dumpexports and copy them off-server to a remote storage location for disaster recovery.
Closing Thoughts
Running Mattermost on your own VPS means your team’s communication stays exactly where it belongs — on infrastructure you control. No per-seat fees that balloon as headcount grows. No vendor deciding to restructure pricing, sunset a feature, or disappear mid-contract. Just a fast, reliable messaging platform that’s entirely yours.
The setup you’ve built here is production-ready: containerized for predictable updates, fronted by Nginx with full HTTPS and WebSocket support, and configured with proper file descriptor limits for real-world load. It comfortably handles teams of dozens up to hundreds of concurrent users. And when you’re ready to scale further, Mattermost’s architecture supports high-availability clustering — something worth exploring as your organization grows.


