Install Matelot
Matelot runs as one cell per organization on infrastructure you control: the API, the worker, the web app, a managed runner, a container registry, Postgres, Redis and Keycloak. Nothing is shared with anyone else and nothing leaves the cell except the integrations you connect. Two ways to install it:
| Option A — Docker Compose (recommended to start) | Option B — Kubernetes (Helm) | |
|---|---|---|
| For | evaluation, small teams, one host | production, several teams, your platform standards |
| Needs | Docker 24+ with Compose v2, 2 vCPU / 4 GB, a port (80/443), optionally a DNS name | a cluster, ingress, TLS, storage class, a wildcard DNS record |
| Time to first login | about five minutes | an afternoon the first time |
| Availability | single host; restart = short downtime | replicas for the API and web, Postgres on a volume or a managed database |
| TLS | automatic (Let's Encrypt via Caddy) with a real domain; plain HTTP on localhost | cert-manager or your own certificates |
| Backups | cell-backup.sh --compose (databases + secrets + registry blobs), cron on the host |
CronJob in the chart to a volume and/or S3 |
| Upgrades | change MATELOT_VERSION, docker compose pull && up -d |
helm upgrade --version |
| When to move | when you need HA, more than one runner pool on the same host, or company Kubernetes policies — a Compose backup restores into a Helm cell (same archive format) |
Both use the same images (ghcr.io/matelot-cloud/{api,worker,web,runner}), the same release, and the same
backup archive, so you can start with Compose and move later without losing anything.
Option A: Docker Compose
A1. Requirements
- Docker Engine 24 or newer with the Compose plugin v2 (
docker compose version), on Linux, macOS or WSL 2. - 2 vCPU and 4 GB of memory (the stack is limited to about 4 GB; Keycloak is the largest part), 20 GB of disk.
- Ports 80 and 443 free (configurable), plus 5000 when you use the registry on localhost.
- For HTTPS: a DNS name for the host (
matelot.example.com) andregistry.matelot.example.com, both pointing at it; Caddy obtains Let's Encrypt certificates on first start. Without a domain everything runs onhttp://localhost.
A2. Start it
git clone --depth 1 https://github.com/matelot-cloud/matelot && cd matelot/docker # or download the docker/ folder
cp .env.example .env
# edit .env: MATELOT_DOMAIN (keep localhost to try it), MATELOT_ADMIN_EMAIL, optionally MATELOT_ADMIN_PASSWORD
docker compose up -d
docker compose logs -f welcome # prints the URL and the administrator login once the cell answers
The first start pulls the images, generates every secret (database and Keycloak passwords, the cell encryption key,
the registry signing key, the managed-runner secret) into the config volume, runs the database migrations,
imports the Keycloak realm with the first administrator, and starts the managed runner. Nothing in the files is a
default password. Two to five minutes later welcome prints:
Matelot is up: http://localhost
Sign in as: admin@localhost
Password: <generated>
Registry: localhost:5000
Sign in with that e-mail and password (docker compose logs welcome shows them again; docker compose run --rm init show prints them from the volume). The first login of MATELOT_ADMIN_EMAIL becomes administrator of the
root organization; invite the team from Members, or connect your identity provider in Keycloak
(<url>/auth/admin/, user admin, password in the config volume under secrets/keycloak/).
Check it from your machine:
npm install -g https://github.com/matelot-cloud/setup-cli/releases/latest/download/matelot-cli.tgz
matelot login --url http://localhost
matelot doctor # API, version, login, organization, runners online, registry
matelot registry login # docker login to the cell's registry with your credentials
matelot registry info # host and the repository pattern: <workspace>/<team>/<project>/<service>:<tag>
docker push "$(matelot registry path --service <serviceId>)":1 # prints host + path; any other repository name is refused
A3. Settings (.env)
| Variable | Default | What |
|---|---|---|
MATELOT_VERSION |
the release the file shipped with | image tag of the four Matelot images — the bare version (0.1.0, 0.1.0-rc.8), not the v… git tag (images carry both); change it to upgrade |
MATELOT_DOMAIN |
localhost |
hostname users open; a real name switches to HTTPS and registry.<domain> |
MATELOT_ACME_EMAIL |
empty | Let's Encrypt contact |
MATELOT_ADMIN_EMAIL / MATELOT_ADMIN_PASSWORD |
admin@localhost / generated |
the first administrator (password set here only before the first start) |
MATELOT_ORG_SLUG / MATELOT_ORG_NAME |
main / My organization |
the root organization (slug is permanent) |
MATELOT_HTTP_PORT / MATELOT_HTTPS_PORT / MATELOT_REGISTRY_PORT |
80 / 443 / 5000 | published ports |
MATELOT_LICENSE |
empty | licence key (Free tier when empty; entitlements arrive with the commercial release) |
MATELOT_BACKUP_RETENTION_DAYS |
14 | archives kept on the backups volume |
MATELOT_THEME_LOGO / MATELOT_THEME_PRIMARY_COLOR |
empty | the sign-in page's logo (URL or data: URI) and button / link colour (see Branding the sign-in page below) |
COMPOSE_PROFILES |
empty | observability adds Prometheus, Tempo, Loki, Grafana (at <domain>/grafana/, Keycloak sign-in; local admin password in docker compose run --rm init show), Alloy and the exporters — nothing published on the host (docs/observability.md) |
MATELOT_OTLP_ENDPOINT / _PROTOCOL / _HEADERS / MATELOT_TRACES_SAMPLER |
empty | traces to your own collector instead (docs/observability.md) |
Changing MATELOT_DOMAIN (or the scheme) after the first start is just docker compose up -d: Keycloak imported the
realm once, so the one-shot realm-sync service updates the realm's redirect URIs and sslRequired through the admin
API on every start (users untouched; docker compose logs realm-sync). Everything else regenerates on every start.
A4. Backups and restore
scripts/cell-backup.sh --compose . -d ./backups # pg_dump of both databases + secrets + registry blobs → one archive
scripts/cell-backup.sh --compose . --list # archives on the backups volume
scripts/cell-restore.sh --compose . ./backups/main-20261012T140000Z.tar.gz # replaces the cell's data with the archive
Schedule it with cron on the host (17 2 * * * cd /opt/matelot/docker && scripts/cell-backup.sh --compose . -d /backups).
The archive is the same format as the Kubernetes backup, so it restores into a Helm cell too (registry blobs are
restored only into Compose cells). The archive contains the encryption key and every password: keep it where only the
cell's operators can read it. Rebuilding on a new host: copy the docker/ folder and .env, then
cell-restore.sh --compose . ARCHIVE — it recreates the volumes, restores the secrets first, then the databases.
A5. Upgrades and rollback
# edit .env: MATELOT_VERSION=0.2.0
scripts/cell-backup.sh --compose . -d ./backups
docker compose pull && docker compose up -d # migrations run before the new API starts
matelot doctor
Only the four Matelot services restart; Postgres, Keycloak, Redis and the registry keep running. Rollback: set the
previous MATELOT_VERSION and docker compose up -d — this works as long as the newer release added no database
migration (the release notes say so); otherwise restore the backup taken before the upgrade, then roll the version
back. Release notes and image digests: https://github.com/matelot-cloud/matelot/releases.
A6. Removing it
docker compose down # stops the cell, keeps every volume (data, secrets, certificates)
docker compose down -v # removes everything
Option B: Kubernetes (Helm)
B1. Requirements
| Need | Notes |
|---|---|
Kubernetes 1.28 or newer, kubectl and Helm 3.12+ |
any conformant distribution; tested on kind, EKS, GKE and AKS storage defaults |
An ingress controller (class nginx by default) |
the chart creates three Ingresses per cell |
cert-manager with a ClusterIssuer, or your own TLS Secrets |
hostnames <slug>.<domain>, auth-<slug>.<domain>, registry-<slug>.<domain>. TLS is mandatory for any hostname other than localhost: browsers treat only https:// and localhost as secure contexts, and the Keycloak login page needs one (Crypto.subtle is available only in secure contexts). With ingress.tls: false on a real hostname the API and the CLI work but nobody can sign in with a browser; the chart says so in its install notes. For a local cluster, a self-signed cert-manager issuer is enough (click through the certificate warning, or trust the local CA once) |
A DNS record for those hostnames (a wildcard *.<domain> is simplest) |
|
| A default StorageClass | Postgres, the registry and backups use volumes; WaitForFirstConsumer classes are fine |
Outbound access to ghcr.io |
images and the chart are public packages; mirror them for air-gapped clusters |
| Resources for a small cell | about 2 vCPU and 5 GiB of memory requested across all components; Keycloak is the largest |
Optional: an S3-compatible bucket for off-site backups, an SMTP relay for e-mail notifications and member invitations (without one, invitation and password-reset links are shown once to copy), a GitHub App for GitHub repositories (any Git host also works with tokens).
B2. Choose the cell's values
helm show values oci://ghcr.io/matelot-cloud/charts/secops-cell --version 0.1.0 > values.yaml
The settings that matter for a first install:
tenant:
slug: acme # DNS label; hostnames, namespace and the root organization are derived from it
name: "ACME Corporation"
domain: platform.example.com # app: acme.platform.example.com · auth: auth-acme.… · registry: registry-acme.…
adminEmails: ["ciso@acme.com"] # the first login of these e-mails becomes administrator of the root organization
ingress:
className: nginx
tls: true
annotations:
cert-manager.io/cluster-issuer: letsencrypt
postgres:
bundled: true # in-cluster Postgres with a volume; or bundled: false + externalUrl for a managed database
password: "<generate one>"
keycloak:
adminPassword: "<generate one>" # the Keycloak admin console (auth-acme.…); users come from your identity provider
allowPasswordGrant: false
seedUsers: []
backup:
enabled: true
schedule: "17 2 * * *" # daily, UTC
pvc: { enabled: true, size: 20Gi }
s3: # optional off-site copy
enabled: false
Images are ghcr.io/matelot-cloud/{api,worker,web,runner} at the chart's appVersion; leave the image blocks alone
unless you mirror them. Everything else (replicas, resources, managed runner count, SMTP via api.extraEnv) has a
documented default in values.yaml.
Branding the sign-in page. Sign-in, OTP, password-reset and consent pages are Keycloak's, rendered with the
cell's matelot login theme: your organization's name as the heading and in the tab title, the product logo, the
app's colours in light and dark mode. To show your own logo and colour instead, set keycloak.theme.logo (a URL or
data: URI) and keycloak.theme.primaryColor (primaryColorHover optional) in the values file — on Compose
MATELOT_THEME_LOGO / MATELOT_THEME_PRIMARY_COLOR in .env; keycloak.theme.enabled: false restores the stock
Keycloak look. The theme overrides no Keycloak template, so Keycloak upgrades keep working.
Member management needs nothing extra: with the bundled Keycloak the API reaches its admin API in-cluster
(KEYCLOAK_ADMIN_URL, set by the chart and by Compose) to create invited users and set passwords. Optional API
settings: SERVICE_ACCOUNT_TOKEN_MAX_DAYS (longest service-account token, default 365) and
MATELOT_FEATURES_DISABLED (comma-separated capabilities to turn off, see Roles).
B3. Install
helm install acme oci://ghcr.io/matelot-cloud/charts/secops-cell --version 0.1.0 \
--namespace tenant-acme --create-namespace -f values.yaml
kubectl -n tenant-acme get pods -w
The first start takes a few minutes: Keycloak imports its realm, the API's init container runs the database
migrations, the managed runner registers once the API answers. Do not use helm install --wait with backups on a
PVC whose StorageClass binds on first use (kind, EKS gp3, GKE, AKS defaults): that PVC stays Pending until the
first backup mounts it and --wait would wait for it. Instead, when every pod is Running:
kubectl -n tenant-acme create job acme-backup-first --from=cronjob/acme-backup # first backup, binds the volume
kubectl -n tenant-acme logs job/acme-backup-first -f
The chart generates and keeps its own secrets (the encryption key for stored credentials, the registry token key pair, the managed-runner secret); they survive upgrades and are part of every backup.
B4. First login and identity
- The administrators in
tenant.adminEmailsalready exist in Keycloak: the install (and every upgrade) seeds the missing ones with a temporary password, kept in the release's Secret — the first login asks for a new one:
(kubectl -n tenant-acme get secret acme-keycloak -o jsonpath='{.data.SEED_ADMIN_PASSWORD}' | base64 -d; echokeycloak.seedAdminPasswordsets it explicitly;keycloak.seedAdmins: falseturns the seeding off; users that exist are never modified.) Openhttps://acme.platform.example.comand sign in: that first login grants the administrator role on the root organization. Invite the team from Members. - For everyone else, open
https://auth-acme.platform.example.comas the Keycloak admin (keycloak.adminPassword), realmplatform, and add your identity provider (SAML or OIDC: Entra ID, Okta, Google Workspace…). Users are created on first login; the e-mail address is what Matelot keys users on. - The Overview shows the organization setup: connect source code, check the managed runner, alerts, team. From there, create a project and a service, attach a repository and run the security pipeline.
For a quick evaluation without an identity provider, keycloak.seedUsers creates local users (and
keycloak.allowPasswordGrant: true lets the CLI log in with a password); remove both for production.
B5. The CLI
npm install -g https://github.com/matelot-cloud/setup-cli/releases/latest/download/matelot-cli.tgz # pinned: .../releases/download/vX.Y.Z/matelot-cli-X.Y.Z.tgz · GitHub Actions: uses: matelot-cloud/setup-cli@v2
matelot login --url https://acme.platform.example.com
matelot whoami
CI pipelines authenticate with signed CI tokens (created in the web app under Access tokens), never with a user password: see the CI/CD integration page.
B6. Backups
With backup.enabled the cell backs itself up on the schedule: pg_dump of the platform and Keycloak databases plus
the cell's secrets, as one archive on the backups volume and, if configured, in your S3 bucket. The archive
contains the encryption key and passwords: restrict the bucket and the volume to the cell's operators.
Restore, including rebuilding a cell on a new cluster, is described on the Backup and restore page. The order
matters there: the secrets must be restored before helm install, so the restored data can be decrypted.
B7. Upgrades
Each release ships the four images and the chart at the same version. To upgrade:
helm upgrade acme oci://ghcr.io/matelot-cloud/charts/secops-cell --version 0.2.0 \
--namespace tenant-acme -f values.yaml
Take a backup first (the CronJob, or a manual job as above). Migrations run in the API's init container before the new API starts. An image release rolls only the api, worker, web and runner pods; Postgres, Keycloak, Redis and the registry keep running. Release notes and image digests are published with every release at https://github.com/matelot-cloud/matelot/releases; images and the chart are signed with cosign (keyless, GitHub OIDC) and carry SBOM attestations.
B8. Removing a cell
helm uninstall acme --namespace tenant-acme # keeps the volumes (databases, registry, backups) and the secrets
kubectl delete namespace tenant-acme # removes everything
Matelot