Help / Mattermost / Self-hosting (Team Edition)
Mattermost
Self-hosting (Team Edition)
Stand up your own Mattermost Team Edition server with Docker — with the Ubuntu 24.04, Fedora 44, and AlmaLinux 10 differences called out. For curious lab students.
This is a from-scratch guide to running your own Mattermost server — the same software the lab runs in production. It’s aimed at students who want to learn how a real self-hosted service goes together: a database, the app, a reverse proxy, TLS, a firewall, and backups.
We use Mattermost Team Edition, which is free and open source with no user limit. The recipe below is what the lab actually runs in production, trimmed down so one person can stand it up on a single server.
This is a learning exercise on a server you own. Don’t point it at lab infrastructure, and don’t reuse the lab’s domains or secrets. If you just want to use lab chat, see Using lab chat instead.
What you’ll build
A three-piece stack, each part in its own Docker container:
| Piece | Image | Job |
|---|---|---|
| PostgreSQL | postgres:16-alpine | Stores every message, user, and channel. |
| Mattermost | mattermost/mattermost-team-edition | The chat app (web + API), listening on port 8065 internally. |
| Caddy | caddy:2-alpine | The only piece exposed to the internet — terminates HTTPS (auto Let’s Encrypt cert) and forwards to Mattermost. |
Docker is the recommended path because it makes the three operating systems behave almost identically — the only real differences are how you install Docker, the firewall tool, and SELinux. We call each of those out below. A non-Docker (“native”) install is covered last.
Before you start
You need:
- A server running Ubuntu 24.04 LTS, Fedora 44, or AlmaLinux 10. Minimum 2 GB RAM for a handful of users; 4 GB is comfortable. ~10 GB free disk to start. RAM scales with the number of people connected at once.
sudoon that server.- A domain name you control (e.g.
chat.example.edu) with a DNSArecord pointing at the server’s public IP. TLS won’t work without this — set it up first and confirmping chat.example.eduresolves to your server. - Ports 80 and 443 reachable from the internet. Port 80 is required so Caddy can complete the Let’s Encrypt certificate challenge; 443 is the actual site.
The OS differences at a glance
Everything in this guide is identical across the three systems except the rows below. Wherever a step differs, pick the block for your OS.
| Ubuntu 24.04 LTS | Fedora 44 | AlmaLinux 10 | |
|---|---|---|---|
| Package manager | apt | dnf (dnf5) | dnf (dnf5) |
| Firewall | ufw (inactive by default) | firewalld (on) | firewalld (on) |
| Mandatory access control | AppArmor — nothing to do | SELinux enforcing | SELinux enforcing |
| Default container engine | none | Podman (we install Docker) | Podman (we install Docker) |
| Docker repo flavor | ubuntu | fedora | centos |
| Native Postgres package | postgresql | postgresql-server | postgresql-server |
Postgres needs manual initdb | no | yes | yes |
The two that bite people: firewalld vs ufw, and SELinux on the Red Hat family (Fedora, AlmaLinux). Both are handled below.
Fast path: download an install script
If you’d rather not run the steps by hand, each OS below has a script that does
all of Steps 1–5 for you — installs Docker, opens the firewall, writes the
stack files into /opt/mattermost (generating a random database password), and
starts the server. Pick the one for your OS:
- Ubuntu 24.04 —
mattermost-install-ubuntu2404.sh - Fedora 44 —
mattermost-install-fedora44.sh - AlmaLinux 10 —
mattermost-install-almalinux10.sh
Download, read it, then run it with your domain:
sudo bash mattermost-install-ubuntu2404.sh chat.example.edu
⚠️ Use at your own risk. These scripts are provided as-is, with no warranty, and NUILab does not guarantee they work or stay current. They make real changes to your machine (install packages, edit the firewall, start services). Run them only on a server you own and can afford to wipe, and read the script first so you know what it does. Everything they automate is spelled out step by step below — working through it by hand is the better way to actually learn it.
Step 1 — Install Docker
Install Docker Engine + the Compose plugin from Docker’s official repository. The commands differ only by OS.
Ubuntu 24.04
sudo apt-get update
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-compose-plugin
Fedora 44
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
dnf5 note. Fedora 44 ships dnf5, where the command is
config-manager addrepo --from-repofile=<url>(shown above). On older dnf it wasconfig-manager --add-repo <url>. If Docker hasn’t published Fedora 44 packages yet (common right after a Fedora release), either wait a few weeks or use Fedora’s own engine:sudo dnf install -y moby-engine docker-compose-plugin. Fedora users can also skip Docker entirely and use Podman (podman+podman-compose); the compose file below works with both, but watch the SELinux note in Step 3.
AlmaLinux 10
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
AlmaLinux is a RHEL rebuild, so it uses Docker’s CentOS repo. AlmaLinux 10 also uses dnf5 (same
addreposyntax as Fedora).
Then, on every OS
Enable the service and let your user run Docker without sudo:
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
Log out and back in (or run newgrp docker) so the group takes effect, then check:
docker run --rm hello-world
docker compose version
Step 2 — Open the firewall
You only need to allow 80 and 443 inbound (plus SSH). This is the biggest day-to-day difference between the OSes.
Ubuntu (ufw)
ufw is inactive by default, so allow SSH before enabling it or you can lock yourself out:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
Fedora / AlmaLinux (firewalld)
firewalld is already on and already permits SSH:
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
If your server is in a cloud (DigitalOcean, AWS, etc.), there’s usually a second firewall in the provider’s control panel. Open 80/443 there too.
Step 3 — Create the stack files
Make a working directory and create three files in it.
mkdir -p ~/mattermost && cd ~/mattermost
.env — your secrets (never commit this)
Put a strong, unique database password here. Keep this file private.
POSTGRES_PASSWORD=change-me-to-a-long-random-string
Lock it down:
chmod 600 .env
docker-compose.yml
Replace https://chat.example.edu with your domain. Everything else can stay.
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: mmuser
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: mattermost
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mmuser -d mattermost"]
interval: 10s
timeout: 5s
retries: 5
mattermost:
image: mattermost/mattermost-team-edition:11.7 # pin a version; check Docker Hub for the latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
MM_SQLSETTINGS_DRIVERNAME: postgres
MM_SQLSETTINGS_DATASOURCE: "postgres://mmuser:${POSTGRES_PASSWORD}@postgres:5432/mattermost?sslmode=disable&connect_timeout=10"
MM_SERVICESETTINGS_SITEURL: https://chat.example.edu
MM_SERVICESETTINGS_ENABLELOCALMODE: "true" # lets you run admin commands with mmctl --local
volumes:
- mattermost-data:/mattermost/data
- mattermost-config:/mattermost/config
- mattermost-logs:/mattermost/logs
- mattermost-plugins:/mattermost/plugins
- mattermost-client-plugins:/mattermost/client/plugins
- mattermost-bleve:/mattermost/bleve-indexes
networks:
- backend
- frontend
caddy:
image: caddy:2-alpine
restart: unless-stopped
depends_on:
- mattermost
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro # Fedora/AlmaLinux + SELinux: change to ":ro,Z"
- caddy-data:/data # holds the TLS cert + ACME account — keep it!
- caddy-config:/config
networks:
- frontend
networks:
backend:
frontend:
volumes:
postgres-data:
mattermost-data:
mattermost-config:
mattermost-logs:
mattermost-plugins:
mattermost-client-plugins:
mattermost-bleve:
caddy-data:
caddy-config:
Why this shape: Postgres sits on a backend network only, so the database is
never reachable from the internet — only Mattermost can talk to it. Mattermost
has no ports: entry, so it’s reachable only through Caddy on the frontend
network. Caddy is the single front door. This is the same isolation the lab uses.
Caddyfile
Again, swap in your domain. This is the whole file:
chat.example.edu {
reverse_proxy mattermost:8065
}
Caddy automatically requests and renews a free Let’s Encrypt certificate, and it forwards WebSocket connections (Mattermost’s live updates) with no extra config.
SELinux (Fedora / AlmaLinux only). The compose file bind-mounts
Caddyfileinto the container. On SELinux systems an unlabeled host file is denied inside the container, so the cert step fails with a permission error. Fix: add theZrelabel flag — change that one line to- ./Caddyfile:/etc/caddy/Caddyfile:ro,Z. The named volumes (everything else) are already labeled correctly and need nothing. On Ubuntu, leave it as:ro.
Step 4 — Start it
docker compose up -d
docker compose ps
All three services should show as running (Postgres healthy). Watch the app come
up, and watch Caddy fetch your certificate:
docker compose logs -f
Give it 30–60 seconds, then open https://chat.example.edu in a browser. You
should get a valid padlock and the Mattermost sign-up screen.
Cert didn’t issue? It’s almost always DNS or the firewall. Confirm the
Arecord points at this server and that port 80 is open both in your OS firewall and any cloud firewall. Thendocker compose restart caddyand watch the logs again.
Step 5 — Create your admin account and team
- On the sign-up screen, create the first account. The very first user on a fresh server automatically becomes the System Admin.
- Create your first Team (the top-level workspace).
- Open the System Console (top-left menu → System Console) to configure the server. The most important settings are already set via the compose file (Site URL, database), so you can largely leave it alone to start.
Admin from the command line (optional)
Because we set local mode on, you can administer the server without the web UI:
docker compose exec mattermost mmctl --local user list
docker compose exec mattermost mmctl --local user create --email you@example.edu --username admin --password 'a-strong-password' --system-admin
mmctl --local is the same tool the lab uses for bulk operations.
Email (so password resets and invites work)
Out of the box your server can’t send email, so “Forgot password” and email
invitations won’t work — fine while you’re the only user, but you’ll want it for
anyone else. Point Mattermost at any SMTP relay (your university’s, or a free tier
from Brevo, Mailgun, etc.). Add these to the mattermost service environment: in
the compose file, then re-run docker compose up -d:
MM_EMAILSETTINGS_SENDEMAILNOTIFICATIONS: "true"
MM_EMAILSETTINGS_ENABLESMTPAUTH: "true"
MM_EMAILSETTINGS_SMTPSERVER: smtp-relay.example.com
MM_EMAILSETTINGS_SMTPPORT: "587"
MM_EMAILSETTINGS_CONNECTIONSECURITY: STARTTLS
MM_EMAILSETTINGS_SMTPUSERNAME: your-smtp-login
MM_EMAILSETTINGS_SMTPPASSWORD: your-smtp-key
MM_EMAILSETTINGS_FEEDBACKEMAIL: noreply@example.edu
MM_EMAILSETTINGS_FEEDBACKNAME: Mattermost
You can also set all of this in System Console → Environment → SMTP and click
Test Connection. (Keep real credentials in .env and reference them as
${VARNAME} rather than pasting them into the compose file.)
Backups
Two things hold all your data; back up both. The other volumes (logs, config, plugins) are rebuildable.
- The database (every message and user). Dump it to a compressed file:
docker compose exec -T postgres pg_dump -U mmuser mattermost | gzip > mm-db-backup.sql.gz
- Uploaded files. With this guide, files live in the
mattermost-datavolume. Back that volume up:
docker run --rm -v mattermost_mattermost-data:/data -v "$PWD":/backup alpine tar czf /backup/mm-files-backup.tar.gz -C /data .
(The volume name is <projectfolder>_mattermost-data — here the folder is
mattermost, so mattermost_mattermost-data. Confirm with docker volume ls.)
Run these on a schedule with cron, and copy the files off the server. For a
bigger setup you can switch file storage to S3-compatible object storage in System
Console, which the lab does.
Updating Mattermost
Mattermost ships frequent releases. To update, bump the image tag and pull:
- Back up the database first (above).
- Edit
docker-compose.ymland change the tag, e.g.:11.7→ the newer version. - Apply it:
docker compose pull
docker compose up -d
The app runs any needed database migrations on startup. Read the release notes (opens in new tab) before a major version jump, and don’t skip across several major versions at once.
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
| No HTTPS / cert error | DNS not pointing here, or port 80 blocked (OS and cloud firewall). Check docker compose logs caddy. |
| Caddy: permission denied reading Caddyfile | SELinux on Fedora/AlmaLinux — add ,Z to the Caddyfile bind mount (Step 3). |
| App keeps restarting | Usually the DB. Check docker compose logs mattermost for connection errors; confirm POSTGRES_PASSWORD matches in .env. |
| “We could not connect” in browser | App still starting, or you exposed it wrong. Mattermost should have no ports: — Caddy is the only public service. |
mmctl --local fails | Local mode must be on (it is, via the compose env) and you must run it inside the container with docker compose exec. |
| Page loads over IP but no padlock | You browsed the raw IP. Always use the domain — the cert is issued for the domain, not the IP. |
Alternative: a native (non-Docker) install
If you want to learn the moving parts without containers, you can install Mattermost straight onto the OS. This is where the operating systems differ the most. The outline:
1. Install and prepare PostgreSQL.
- Ubuntu —
sudo apt-get install -y postgresql(it auto-initializes and starts). - Fedora / AlmaLinux —
sudo dnf install -y postgresql-server postgresql, then initialize it yourself:sudo postgresql-setup --initdb, thensudo systemctl enable --now postgresql. On the Red Hat family you must also edit/var/lib/pgsql/data/pg_hba.confto usescram-sha-256(notident) for local connections, thensudo systemctl restart postgresql, or password login will be refused.
Then create the database and user (all OSes):
sudo -u postgres psql -c "CREATE USER mmuser WITH PASSWORD 'a-strong-password';"
sudo -u postgres psql -c "CREATE DATABASE mattermost OWNER mmuser;"
2. Install the Mattermost server. Download the Team Edition tarball (check
mattermost.com (opens in new tab)
for the current version),
extract it to /opt, create a service account, and a data directory:
wget https://releases.mattermost.com/11.7.0/mattermost-team-11.7.0-linux-amd64.tar.gz
sudo tar -xzf mattermost-team-11.7.0-linux-amd64.tar.gz -C /opt
sudo useradd --system --user-group mattermost
sudo mkdir /opt/mattermost/data
sudo chown -R mattermost:mattermost /opt/mattermost
3. Configure it. Edit /opt/mattermost/config/config.json and set
SqlSettings.DriverName to postgres, SqlSettings.DataSource to
postgres://mmuser:a-strong-password@localhost:5432/mattermost?sslmode=disable&connect_timeout=10,
and ServiceSettings.SiteURL to https://chat.example.edu.
4. Run it under systemd. Create /etc/systemd/system/mattermost.service:
[Unit]
Description=Mattermost
After=network.target postgresql.service
[Service]
Type=notify
ExecStart=/opt/mattermost/bin/mattermost
WorkingDirectory=/opt/mattermost
User=mattermost
Group=mattermost
LimitNOFILE=49152
[Install]
WantedBy=multi-user.target
Then sudo systemctl daemon-reload and sudo systemctl enable --now mattermost.
5. Put a reverse proxy in front for TLS. Install Caddy (simplest — auto-HTTPS)
or nginx + certbot, and proxy your domain to http://localhost:8065. A host Caddy
needs just the same two-line Caddyfile shown above but pointing at
localhost:8065.
SELinux for a native proxy (Fedora / AlmaLinux). If you use nginx, SELinux blocks it from making outbound connections to Mattermost until you run
sudo setsebool -P httpd_can_network_connect 1. Ubuntu has no equivalent step.
The Docker path is recommended for most students because it bundles all of the above into three files and behaves the same on every OS. The native path is worth doing once to understand what Docker is automating.
Team Edition vs Enterprise
Everything here uses Team Edition — free, open source, unlimited users, and plenty for a lab or class. It includes channels, DMs, threads, search, integrations/bots, webhooks, and the desktop/mobile apps. The paid Enterprise tier adds things you generally won’t need for a student project: SAML/LDAP/AD single sign-on, advanced compliance and data retention, fine-grained permissions and guest accounts, and high-availability clustering. You can run Team Edition indefinitely and upgrade later with no data migration.
Further reading
- Mattermost documentation (opens in new tab) — the official manual.
- Install guides (opens in new tab) — every supported method in detail.
mmctlreference (opens in new tab) — the command-line admin tool used above.
Source: content/systems/mattermost/self-hosting.md · maintained in the lab docs repository.