Mattermost on Kubernetes without Helm, with Kapitan
Mattermost Team Edition 12.0.2, a team chat server with channels, direct messages and file sharing, with its files on a claim, rendered by the kadet-hub kubernetes generator 1.14.0 from Mattermost’s Helm chart mattermost-team-edition 6.6.111. The database is the included recipe postgresql.
- Latest version:
1.0.0, published - Source chart:
https://helm.mattermost.com/mattermost-team-edition:6.6.111@sha256:ec3f1f44bd767ee25a4e3a32270ebcce428aafa2fd4d0c6247a1a41dc7e61851 - Proof level:
deploys, compared with the chart by the kadet-hub/recipes CI renderspassed in the release run of with check 0.18.0, kind v0.33.0- Output capabilities: none
- Inventory backends:
omegaconf - Prerequisites, installed once per cluster before the recipe:
cnpg - Generators: kubernetes 1.14.0
sha256:787129e1eaed6b9bc868fe4c4aec58d40de6a89fc3cc08424a4cb5822b855080 - Image security: grade D 58 findings, one per advisory and package, in the 2 images of version 1.0.0: CRITICAL 0, HIGH 5, MEDIUM 33, LOW 14, UNKNOWN 6. Scanned 2026-11-07 11:47 UTC. Full report
The grade rates the upstream images the recipe pins, not the recipe:docker.io/curlimages/curl,docker.io/mattermost/mattermost-team-edition.
What it deploys
recipes.mattermost(init.yml)recipes.mattermost.mattermost(mattermost.yml):mattermost-admin,mattermost-mattermost-team-edition
Required parameters
applications.mattermost.admin.username,
applications.mattermost.admin.email and
applications.mattermost.admin.password are the first admin’s account.
Set the password as a ref with the backend of your choice. Mattermost
requires at least 8 characters. Without one of the parameters the compile
stops with Interpolation key '<key>' not found. postgresql requires no
parameter.
Include
A target includes the recipe and its options as classes and can override anything the recipe sets:
classes:
- recipes.mattermost
parameters:
namespace: mattermost
kapitan:
vars:
target: mattermost
applications:
mattermost:
admin:
username: admin
email: admin@example.com
password: <ref>
Fetching it
The quickstart goes through these steps on a kind cluster with the uptime-kuma recipe, from installing Kapitan to the running app.
A separate fetch inventory pins the recipe by version and digest. This is fetch/targets/recipes.yml for version 1.0.0:
parameters:
kapitan:
dependencies:
- type: oci
source: ghcr.io/kadet-hub/recipes/mattermost:1.0.0@sha256:398f87bf0a7f9ff5bdff6bd3a4ec25c852db9beee801808e2b0c37c48390ca16
output_path: inventory/classes/recipes/mattermost
Version 1.0.0 includes postgresql 1.1.1. The consumer workflow adds the included recipes at the digests the index lists. Kapitan's --fetch reads no index, so for a local compile pull them by digest first:
tmp=$(mktemp -d)
oras pull -o "$tmp" ghcr.io/kadet-hub/recipes/postgresql@sha256:1e8465058c256cc86995dc8d6f1eb2f095df244fadcaf756935599f4d9ead6a9
mkdir -p inventory/classes/recipes/postgresql
tar -xf "$tmp/postgresql.tar" -C inventory/classes/recipes/postgresql --no-same-owner
The first command fetches the recipe into inventory/classes/recipes/mattermost, the second the generators and compiles:
kapitan compile --inventory-path fetch --fetch --output-path .
kapitan compile --fetch
The first command exits with status 1 because the fetch target compiles nothing. Kapitan's --fetch checks neither the digest nor the attestation. This verifies the attestation of the recipe and of the recipes it includes:
gh attestation verify \
oci://ghcr.io/kadet-hub/recipes/mattermost@sha256:398f87bf0a7f9ff5bdff6bd3a4ec25c852db9beee801808e2b0c37c48390ca16 \
--owner kadet-hub \
--cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main
gh attestation verify \
oci://ghcr.io/kadet-hub/recipes/postgresql@sha256:1e8465058c256cc86995dc8d6f1eb2f095df244fadcaf756935599f4d9ead6a9 \
--owner kadet-hub \
--cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main
The attested index must also list each digest under its recipe with the pinned version, and not as yanked. Verifying by hand shows how to read it.
Settings
<c> stands for components.mattermost-mattermost-team-edition, <s>
for generators.kubernetes.secrets.
| Chart value | Inventory |
|---|---|
image.tag |
<c>.image, tag and digest |
config |
<s>.mattermost-mattermost-team-edition-config.string_data.<variable>.value, such as MM_SERVICESETTINGS_SITEURL |
extraEnvVars |
<c>.env |
persistence.data.size |
generators.kubernetes.persistent_volume_claims.mattermost-mattermost-team-edition.storage, 10Gi |
persistence.plugins.size |
generators.kubernetes.persistent_volume_claims.mattermost-mattermost-team-edition-plugins.storage, 1Gi |
persistence.*.storageClass |
storage_class next to storage |
service.type |
generators.kubernetes.services.mattermost-team-edition.type, ClusterIP |
resources |
<c>.resources |
nodeSelector, tolerations, affinity |
<c>.node_selector, <c>.tolerations, <c>.affinity |
externalDB.externalConnectionString |
<s>.mattermost-mattermost-team-edition-mattermost-dbsecret.string_data.mattermost.dbsecret.value, from applications.postgresql |
Mattermost keeps its configuration in the database. A variable
MM_<SECTION>_<SETTING> in the Secret mattermost-mattermost-team-edition-config
overrides the setting and locks it in the System Console. Set
MM_SERVICESETTINGS_SITEURL to the URL users reach Mattermost at. Without
it Mattermost logs at start that some features will operate incorrectly.
The pod annotation checksum/config is the chart’s hash of that Secret
for the values of tests/values.yaml. It does not follow the target’s
values, so after changing the Secret restart the Deployment with
kubectl rollout restart.
Both claims are ReadWriteOnce, and the Deployment rolls out a new pod before the old one stops. On a cluster where the new pod lands on another node it waits for the claim until the old pod is gone. The chart’s Ingress, HTTPRoute and MySQL subchart are not part of the recipe.
Any list set in the target is appended to the recipe’s list. To replace
one, set the key to ~ in a class included after the recipe and the new
list in the target.
Proof
check mattermost --level deploys with tests/values.yaml and
tests/target.yml, on kind for each Kubernetes minor CI tests, with
cnpg installed first. The objects of postgresql are left out of the
comparison and applied with the recipe. The Deployment and the Service
select on name, the chart on app.kubernetes.io/name, instance and
managed-by, and both select the same pods. The container is named after
the component, the chart’s mattermost-team-edition. The pod sets
automountServiceAccountToken: true, the API default the chart leaves
unset. The chart binds no role. Its Helm test
Pod/mattermost-mattermost-team-edition-test is left out.
tests/values.yaml adds the Job mattermost-admin through the chart’s
extraDeploy, as a user would next to the chart. The Secret
mattermost-admin with its password is only in the recipe, and the
chart’s side reads it.
tests/target.yml runs one PostgreSQL instance.
tests/smoke checks that the pods pass Pod Security restricted and that
/api/v4/system/ping answers through the Service. It waits for the Job
mattermost-admin and logs in as its admin. Through the API the client
configuration names the image tag and a Team Edition build, and a sign-up
without an invitation gets 403. The admin creates a team, a channel and a
second user, and posts a message with a file. The second user reads both,
and reads them again after the test deleted the pod and its successor is
ready, which needs the database and the claim. The test also runs the Job
again, which completes without a new account, and the admin still logs
in.
Versions
1.0.0, :sha256:398f87bf0a7f9ff5bdff6bd3a4ec25c852db9beee801808e2b0c37c48390ca16
The recipe's files are in kadet-hub/recipes. All recipes