Mattermost team chat
Mattermost team chat
This pattern composes Compute, Network, and Block Storage into a self-hosted team-chat platform for a team, on infrastructure you control.
What this template does#
Provisions a single instance running Mattermost Team Edition, an open-source team-chat platform (a self-hosted alternative to Slack). Your team communicates over channels, direct messages, and integrations on infrastructure you own:
- Compute instance that runs Mattermost in Docker alongside a bundled PostgreSQL (4 vCPU and 4 GiB RAM, a heavier floor than the lighter ops-tools templates in this library)
- Private network, subnet, router, port, and security group; a floating IP for public access
- A block volume mounted at
/var/lib/docker, so the database data and every Mattermost data directory (config, data, logs, plugins, search indexes) live on a volume you can grow - cloud-init installs Docker Engine, brings up PostgreSQL, and prepares Mattermost to start once you finish configuration
The PostgreSQL password is generated on first boot and written to /opt/mattermost/.env; no credential ships with this template.
Mattermost needs a public URL before it is fully usable#
Like Infisical and Plane, Mattermost's MM_SERVICESETTINGS_SITEURL must point at a real public HTTPS address before invite links, OAuth, and calls work. This template holds the Mattermost app container until you finish that configuration; PostgreSQL starts immediately.
Parameters#
| Parameter | Description | Default |
|---|---|---|
key_name | SSH keypair name (must already exist) | No default |
flavor_name | Instance size (Mattermost plus PostgreSQL runs on 4 vCPU / 4 GiB for a small-to-mid team) | s1a.medium |
image_name | Operating system image | Ubuntu-24.04 |
app_name | Display name prefix for resources | mattermost |
volume_size | Block volume size in GiB, mounted at /var/lib/docker | 20 |
external_network | External network for floating IP allocation | PublicStatic |
private_cidr | CIDR for the private subnet | 10.55.0.0/24 |
app_allowed_cidr | CIDR allowed to reach Mattermost on port 8065 | 10.55.0.0/24 |
Finish setup after apply#
cloud-init starts PostgreSQL and writes the generated database password to /opt/mattermost/.env. Complete the setup over SSH:
- Point a domain's DNS A record at the floating IP and put a reverse proxy (Caddy or Nginx) in front for HTTPS on 443.
- Edit
/opt/mattermost/.env: setMM_SERVICESETTINGS_SITEURLto your public HTTPS address. - Start Mattermost:
cd /opt/mattermost
sudo docker compose up -d- Open the site URL and sign up the first admin account, which becomes the System Console admin.
Access and security#
Mattermost listens on port 8065 over plain HTTP. The security group restricts 8065 to app_allowed_cidr, which defaults to the private network only. Because Mattermost needs a public URL for invite links, OAuth, and calls, the normal access path is a domain with HTTPS on 443 behind a reverse proxy. Ports 80 and 443 stay open for that proxy; they carry no traffic until you add one.
When to use this pattern#
Run team chat and channel-based communication for a team on a host you operate. This template's default sizing suits a small-to-mid team; Mattermost's own scaling guidance recommends significantly more compute for large enterprise deployments, and splitting PostgreSQL onto its own instance as the team grows. To run the database separately from the start, point MM_SQLSETTINGS_DATASOURCE at a self-managed PostgreSQL instance.
Estimated cost#
Monthly cost estimate
Pricing calculator ↗Sized as a custom package on shared vCPU.
Monthly total for the required template above. Use the configurator below to add optional pieces and see the total update.
What each resource is for
Mattermost team-chat host
s1a.medium · 4 shared vCPU, 4 GiB RAM, 0.5 Gbps
Runs Mattermost's app container alongside a bundled PostgreSQL, with the database data and every Mattermost data directory on an attached volume.
Mattermost plus its bundled PostgreSQL runs on 4 vCPU and 4 GiB RAM for a small-to-mid team. Mattermost's own scaling guidance recommends significantly more for large enterprise deployments.
Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.
Included in baseline
s1a.medium
4 shared vCPU, 4 GiB RAM, 0.5 Gbps
Compute + RAM rate basis
4 vCPU + 4 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.
Block storage (50 GiB)
50 GiB at $0.08/GiB/mo
Public IP (included)
1 included with the custom package
Package promotional discount
Flat −$5.00/mo on the custom package (same promotion as named plans).
Included at no charge
These line items are zero on Quake AI. Many other providers meter them separately.
Data transfer (inbound and outbound)
Unlimited data transfer on every plan; Quake AI does not meter per-GB egress.
AWS, GCP, and Azure meter outbound transfer per GB. DigitalOcean and Hetzner include an allowance on compute plans, then charge overage.
Learn morePrivate networking
Private networks, subnets, Neutron routers, and security groups are included with the plan.
VPC objects are usually free to create elsewhere, but NAT gateways bill hourly plus per-GB processed. Quake AI uses router SNAT with no separate NAT line item.
Control-plane API requests
OpenStack API calls for provisioning and management are included.
Some managed services on other clouds meter API calls or charge for premium control-plane features.
Pricing data last validated: . For current rates, check quake.ai/pricing.
Template source#
Show source (7 files)Hide source
data "openstack_images_image_v2" "os" {
name = var.image_name
most_recent = true
}
data "openstack_networking_network_v2" "external" {
name = var.external_network
}
resource "openstack_networking_network_v2" "private" {
name = "${var.app_name}-net"
admin_state_up = true
}
resource "openstack_networking_subnet_v2" "private" {
name = "${var.app_name}-subnet"
network_id = openstack_networking_network_v2.private.id
cidr = var.private_cidr
ip_version = 4
dns_nameservers = ["1.1.1.1", "8.8.8.8"]
}
resource "openstack_networking_router_v2" "main" {
name = "${var.app_name}-router"
external_network_id = data.openstack_networking_network_v2.external.id
}
resource "openstack_networking_router_interface_v2" "private" {
router_id = openstack_networking_router_v2.main.id
subnet_id = openstack_networking_subnet_v2.private.id
}
resource "openstack_networking_secgroup_v2" "mattermost" {
name = "${var.app_name}-sg"
description = "SSH and HTTP/HTTPS for a reverse proxy; app port 8065 restricted"
}
resource "openstack_networking_secgroup_rule_v2" "ssh" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.mattermost.id
}
# 80 and 443 carry Mattermost once it is served over a domain with automatic
# TLS through a reverse proxy (Caddy or Nginx). Required for production use:
# invite links, OAuth, and calls all depend on MM_SERVICESETTINGS_SITEURL
# being a real public HTTPS address.
resource "openstack_networking_secgroup_rule_v2" "http" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 80
port_range_max = 80
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.mattermost.id
}
resource "openstack_networking_secgroup_rule_v2" "https" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 443
port_range_max = 443
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.mattermost.id
}
# Raw app HTTP on 8065 is restricted to app_allowed_cidr (the private network
# by default). Use it for setup over an SSH tunnel or a scoped workstation IP;
# put a reverse proxy on 443 in front for routine access.
resource "openstack_networking_secgroup_rule_v2" "app" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 8065
port_range_max = 8065
remote_ip_prefix = var.app_allowed_cidr
security_group_id = openstack_networking_secgroup_v2.mattermost.id
}
resource "openstack_networking_port_v2" "mattermost" {
name = "${var.app_name}-port"
network_id = openstack_networking_network_v2.private.id
security_group_ids = [openstack_networking_secgroup_v2.mattermost.id]
fixed_ip {
subnet_id = openstack_networking_subnet_v2.private.id
}
depends_on = [openstack_networking_router_interface_v2.private]
}
resource "openstack_blockstorage_volume_v3" "data" {
name = "${var.app_name}-data"
size = var.volume_size
}
resource "openstack_compute_instance_v2" "mattermost" {
name = var.app_name
flavor_name = var.flavor_name
key_pair = var.key_name
user_data = templatefile("${path.module}/cloud-init/mattermost.yaml.tftpl", {
app_name = var.app_name
})
block_device {
uuid = data.openstack_images_image_v2.os.id
source_type = "image"
destination_type = "volume"
volume_size = 30
boot_index = 0
delete_on_termination = true
}
network {
port = openstack_networking_port_v2.mattermost.id
}
}
resource "openstack_compute_volume_attach_v2" "data" {
instance_id = openstack_compute_instance_v2.mattermost.id
volume_id = openstack_blockstorage_volume_v3.data.id
}
resource "openstack_networking_floatingip_v2" "mattermost" {
pool = var.external_network
}
resource "openstack_networking_floatingip_associate_v2" "mattermost" {
floating_ip = openstack_networking_floatingip_v2.mattermost.address
port_id = openstack_networking_port_v2.mattermost.id
}
variable "key_name" {
description = "SSH keypair name (must already exist in your project)"
type = string
}
variable "flavor_name" {
description = "Instance size. Mattermost plus its bundled PostgreSQL runs on 4 vCPU and 4 GiB RAM (s1a.medium) for a small-to-mid team. Mattermost's own scaling guidance recommends significantly more for large enterprise deployments; size up accordingly."
type = string
default = "s1a.medium"
}
variable "image_name" {
description = "Operating system image. Ubuntu 24.04 is the recommended base."
type = string
default = "Ubuntu-24.04"
}
variable "app_name" {
description = "Display name prefix for compute and network resources"
type = string
default = "mattermost"
}
variable "volume_size" {
description = "Block volume size in GiB, mounted at /var/lib/docker so the PostgreSQL data and Mattermost's config/data/logs/plugins/bleve-indexes directories live on a volume you can grow rather than on the boot disk."
type = number
default = 20
}
variable "external_network" {
description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
type = string
default = "PublicStatic"
}
variable "private_cidr" {
description = "CIDR for the private tenant network the instance lives in"
type = string
default = "10.55.0.0/24"
}
variable "app_allowed_cidr" {
description = "CIDR allowed to reach Mattermost on port 8065. Defaults to the private network only, so the app is not exposed to the public internet on its raw port. Put a reverse proxy on 443 in front for HTTPS once you point a domain at the instance. To reach port 8065 directly from your workstation during setup, set this to YOUR_IP/32."
type = string
default = "10.55.0.0/24"
}
output "instance_id" {
description = "ID of the compute instance running Mattermost"
value = openstack_compute_instance_v2.mattermost.id
}
output "floating_ip" {
description = "Public floating IP address of the Mattermost host"
value = openstack_networking_floatingip_v2.mattermost.address
}
output "private_ip" {
description = "Private IP address of the instance"
value = openstack_compute_instance_v2.mattermost.access_ip_v4
}
output "app_url" {
description = "Mattermost app URL on port 8065. Reachable from app_allowed_cidr (the private network by default). cloud-init starts only PostgreSQL; set MM_SERVICESETTINGS_SITEURL to your public HTTPS address in /opt/mattermost/.env and start the mattermost container yourself, mirroring the Infisical and Plane setup pattern."
value = "http://${openstack_networking_floatingip_v2.mattermost.address}:8065"
}
terraform {
required_version = ">= 1.6.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.0"
}
}
}
provider "openstack" {}
# Required: SSH keypair must already exist in your project
key_name = "YOUR_KEY_NAME"
# Recommended: restrict the app port (8065) to your workstation IP for setup.
# Leave unset to keep 8065 reachable only from the private network and tunnel
# over SSH. For production use, put a reverse proxy in front on 443.
# app_allowed_cidr = "203.0.113.10/32"
# flavor_name = "s1a.medium"
# image_name = "Ubuntu-24.04"
# app_name = "mattermost"
# volume_size = 20
# external_network = "PublicStatic"
# private_cidr = "10.55.0.0/24"
#cloud-config
package_update: true
packages:
- ca-certificates
- curl
write_files:
- path: /opt/mattermost/docker-compose.yml
permissions: "0644"
content: |
# Mattermost team chat for ${app_name}. The app listens on port 8065.
# Mattermost needs a stable public URL (MM_SERVICESETTINGS_SITEURL)
# before invite links, OAuth, and calls work: set it in
# /opt/mattermost/.env, then start the app. No credential ships with
# this template: the PostgreSQL password is generated on first boot.
# cloud-init starts only PostgreSQL; bring up Mattermost after you
# finish configuring /opt/mattermost/.env.
services:
mattermost:
image: mattermost/mattermost-team-edition:release-11.9
restart: unless-stopped
user: "2000:2000"
ports:
- "8065:8065"
env_file:
- /opt/mattermost/.env
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
depends_on:
- postgres
postgres:
image: postgres:16-alpine
restart: unless-stopped
env_file:
- /opt/mattermost/.env
volumes:
- mattermost_pg:/var/lib/postgresql/data
volumes:
mattermost_config:
mattermost_data:
mattermost_logs:
mattermost_plugins:
mattermost_client_plugins:
mattermost_bleve:
mattermost_pg:
runcmd:
- |
set -e
# The data volume attaches as /dev/sdb on this platform (not /dev/vdb).
# Mount it at /var/lib/docker before Docker is installed so the
# PostgreSQL data and every Mattermost volume live on the resizable
# volume rather than the boot disk.
DEV=/dev/sdb
for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F -L mattermostdata "$DEV"; fi
mkdir -p /var/lib/docker
mount "$DEV" /var/lib/docker
grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/docker ext4 defaults,nofail 0 2" >> /etc/fstab
# Install Docker Engine plus the compose plugin from Docker's convenience
# script.
curl -fsSL https://get.docker.com | sh
# Generate the bundled database password on first boot. It never leaves
# this instance.
PGPASS=$(openssl rand -hex 24)
umask 077
{
echo "MM_SQLSETTINGS_DRIVERNAME=postgres"
echo "MM_SQLSETTINGS_DATASOURCE=postgres://mattermost:$PGPASS@postgres:5432/mattermost?sslmode=disable&connect_timeout=10"
echo "MM_BLEVESETTINGS_INDEXDIR=/mattermost/bleve-indexes"
echo "POSTGRES_USER=mattermost"
echo "POSTGRES_PASSWORD=$PGPASS"
echo "POSTGRES_DB=mattermost"
echo "# Set MM_SERVICESETTINGS_SITEURL to your public HTTPS address"
echo "# before starting Mattermost:"
echo "# MM_SERVICESETTINGS_SITEURL=https://chat.example.com"
} > /opt/mattermost/.env
chmod 600 /opt/mattermost/.env
# Bring up PostgreSQL only. Mattermost starts after you set
# MM_SERVICESETTINGS_SITEURL in /opt/mattermost/.env and run:
# cd /opt/mattermost && docker compose up -d
cd /opt/mattermost
docker compose up -d postgres
# Mattermost team chat
Single compute instance running [Mattermost](https://mattermost.com) Team Edition, a self-hosted team-chat platform (a self-hosted alternative to Slack) on infrastructure you control. After apply, cloud-init starts the bundled PostgreSQL; you set the public site URL and start the app container yourself.
**Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.
The instance provisions a private network, a floating IP, and a block volume mounted at `/var/lib/docker` so the database data and Mattermost's config, data, logs, plugins, and search-index directories live on a resizable volume.
## Where this fits
Mattermost centralizes team messaging, channels, and integrations for a team, with a System Console for administration, on infrastructure you own rather than on a third-party SaaS.
## Resource floor: heavier than the lighter ops-tools templates in this library
Mattermost's app server plus its bundled PostgreSQL need more headroom than a single-process tool. This template's `s1a.medium` default (4 vCPU, 4 GiB RAM) suits a small-to-mid team. Mattermost's own scaling guidance recommends significantly more for large enterprise deployments: size up the flavor and split PostgreSQL onto its own instance as your team grows.
## Prerequisites
- OpenTofu >= 1.6.0 or Terraform >= 1.6.0
- Quake AI account with OpenStack credentials
- An existing SSH keypair in your project (the value of `key_name` must match that keypair)
## Usage
1. Clone or copy this template directory
2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values
3. Source your OpenStack credentials: `source openrc.sh`
4. Initialize: `tofu init`
5. Preview: `tofu plan`
6. Apply: `tofu apply`
After apply, cloud-init installs Docker, generates the PostgreSQL password into `/opt/mattermost/.env`, and starts only `postgres`. No credential ships with this template: the database password is generated on first boot.
## Finish setup after apply
Mattermost needs a real public URL before invite links, OAuth, and calls work, so cloud-init holds the app container until you finish configuration:
1. Point a domain's DNS A record at `floating_ip` and put a reverse proxy (Caddy or Nginx) in front for HTTPS on 443.
2. Edit `/opt/mattermost/.env`: set `MM_SERVICESETTINGS_SITEURL` to your public HTTPS address.
3. Start Mattermost:
```bash
cd /opt/mattermost
sudo docker compose up -d
```
4. Open the site URL and sign up the first admin account, which becomes the System Console admin.
## Access and security
Mattermost listens on port 8065 over plain HTTP. The security group restricts 8065 to `app_allowed_cidr`, which defaults to the private network only. Ports 80 and 443 stay open for a reverse proxy you add for production use; they carry no traffic until you add one.
## Datastores
This template bundles PostgreSQL as a container on the same instance, which suits a single-team deployment. To run it as a separate service, point `MM_SQLSETTINGS_DATASOURCE` in `/opt/mattermost/.env` at a [self-managed PostgreSQL](/resources/iac-templates/self-managed-postgres) instance and remove the bundled service from the compose file.
## Variables
| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key_name` | string | yes | n/a | SSH keypair name (must already exist in your project) |
| `flavor_name` | string | no | `s1a.medium` | Instance size (Mattermost plus PostgreSQL runs on 4 vCPU / 4 GiB) |
| `image_name` | string | no | `Ubuntu-24.04` | Operating system image |
| `app_name` | string | no | `mattermost` | Display name prefix for resources |
| `volume_size` | number | no | `20` | Block volume size in GiB, mounted at `/var/lib/docker` |
| `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
| `private_cidr` | string | no | `10.55.0.0/24` | CIDR for the private subnet |
| `app_allowed_cidr` | string | no | `10.55.0.0/24` | CIDR allowed to reach Mattermost on port 8065 |
## Outputs
| Name | Description |
| --- | --- |
| `floating_ip` | Public floating IP assigned to the instance |
| `private_ip` | Private IP address of the instance |
| `app_url` | Mattermost app URL on port 8065 |
| `instance_id` | Compute instance ID |
## Scope
This is a single-VM Mattermost host that you operate, not a managed team-chat cloud. It is CPU-only, runs in one region, and bundles PostgreSQL as a container on the same host, sized for a small-to-mid team. You operate the instance, Docker, Mattermost, the database, and the data volume yourself: back them up, patch them, and size up as your team grows.
## Documentation
See also: [self-managed PostgreSQL](/resources/iac-templates/self-managed-postgres), [Infisical secrets management](/resources/iac-templates/infisical-secrets)
Resources, parameters, and variables
key_namerequiredflavor_name="s1a.medium"image_name="Ubuntu-24.04"app_name="mattermost"volume_size=20external_network="PublicStatic"private_cidr="10.55.0.0/24"app_allowed_cidr="10.55.0.0/24"
Customize this pattern#
- Customize a template's image and flavor
- Add a block volume to a template
- Parameterize a template with a tfvars file
See also#
Usage Guidelines
The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.
For the full policy, see Usage Guidelines.
See Also
Terraform and OpenTofu on Quake AI
Prerequisite
Networks
Prerequisite
Authoring IaC templates for Quake AI
Shares: Volumes, Security Groups
Deploy an API gateway with the api-gateway template
Shares: Volumes, Security Groups
Deploy a regional edge cache with the edge-cache template
Shares: Volumes, Security Groups