Zeabur Catalog
A tested catalog for Kubernetes clusters, in two parts:
- Components are what a cluster administrator installs once per cluster: ingress, certificates, metrics, database operators. For each capability the catalog says which upstream chart, which version, which values, and why.
- Templates are applications a project member installs into one namespace:
n8n, Dify, WordPress, PostgreSQL. Each pins its images, sets
resource requests and limits, runs under the Restricted Pod Security
Standard where the application allows it, and says how it scales, how its
data is backed up and what to check before an upgrade. A template names the
components it needs in
requires.
The catalog is data. CI installs and tests every component and template, and
each release publishes one signed bundle that any tool can read to generate
helm install commands, Flux or Argo CD manifests, k3s HelmChart objects or
template installs.
Zeabur reads this catalog to suggest what a cluster is missing; it never installs anything itself.
Layout
Section titled “Layout”components/<capability>/<upstream>.yaml one file per capability × upstreamtemplates/<id>/template.yaml one application: inputs, services, security, scaling, smoke checkstemplates/<id>/manifests.yaml its Kubernetes objectsschema/v1/ JSON Schemas for bothconfig/licenses.yaml OSI-approved SPDX identifiers; opt-in-only exceptionsconfig/kubernetes.yaml Kubernetes versions; kind node version for e2econfig/budget.yaml single-node memory budget (MiB)config/e2e.yaml test-only valuesmeasurements/ idle memory and CPU, written by CItools/ the `catalog` command (Go)docs/ template standards, deploying a templateBoth kinds of file share one envelope:
apiVersion: catalog.zeabur.com/v1kind: Component # or Templatemetadata: name: Traefik Proxy description: Why it is recommended, or what the template runs, in one or two sentences. license: MIT # SPDX homepage: https://traefik.io/traefik/requires: [certificates] # <capability> or <capability>/<upstream>The path is the ID: components/ingress/traefik.yaml is ingress/traefik,
templates/n8n/ is n8n.
Components
Section titled “Components”| Capability | Upstream | single-node | multi-node |
|---|---|---|---|
| ingress | Traefik | recommended | recommended |
| certificates | cert-manager | recommended | recommended |
| observability-operator | VictoriaMetrics operator | recommended | recommended |
| metrics | VictoriaMetrics (k8s stack) | recommended | recommended |
| metrics | metrics-server | recommended | recommended |
| logs | VictoriaLogs (via the operator) | recommended | recommended |
| node-updates | system-upgrade-controller (k3s only) | recommended | recommended |
| intrusion-prevention | CrowdSec | optional | optional |
| volume-backup | K8up | optional | optional |
| volume-backup | Velero (administrators only) | optional | optional |
| event-autoscaling | KEDA | optional | recommended |
| replicated-storage | Longhorn (default storage class on multi-node) | optional | recommended |
| event-autoscaling | KEDA HTTP add-on (beta) | optional | optional |
| uptime-probes | Prometheus blackbox exporter | optional | recommended |
| runtime-security | Falco | optional | optional |
| vulnerability-scanning | Trivy Operator | optional | optional |
| resource-recommendations | Vertical Pod Autoscaler (recommender) | optional | optional |
| remote-access | Tailscale Kubernetes operator | optional | optional |
| config-reload | Stakater Reloader | optional | optional |
| external-secrets | External Secrets Operator (no SecretStore by default) | optional | optional |
| databases | CloudNativePG with the Barman Cloud plugin (PostgreSQL) | optional | optional |
| databases | Percona Operator for MongoDB (server SSPL-1.0, opt-in only) | optional | optional |
| databases | MariaDB Operator (MySQL-compatible) | optional | optional |
| databases | Dragonfly Operator (Redis-compatible; server BUSL-1.1, opt-in only) | optional | optional |
| databases | MOCO (MySQL 8) | optional | optional |
| object-storage | SeaweedFS operator (beta) | optional | optional |
| container-registry | zot | optional | optional |
| image-updates | Keel | optional | optional |
recommended is on by default, optional is offered but off, unsupported
is not offered. Each file's detection rules find an existing installation so
it can be reused instead.
Templates
Section titled “Templates”| ID | Application | Categories |
|---|---|---|
| browserless | Browserless | automation |
| cliproxyapi | CLIProxyAPI | ai |
| dify | Dify | ai |
| error-pages | Error pages | web-servers |
| ghost | Ghost | cms |
| gitea | Gitea | developer-tools |
| halo | Halo | cms |
| hermes-agent | Hermes Agent | ai, automation |
| librechat | LibreChat | ai |
| litellm | LiteLLM | ai |
| lobechat | LobeChat | ai |
| meilisearch | Meilisearch | search |
| memos | Memos | productivity |
| miniflux | Miniflux | productivity |
| n8n | n8n PostgreSQL | automation |
| n8n-ai-brain | n8n (AI Brain) | ai, automation |
| n8n-sqlite | n8n SQLite | automation |
| new-api | New API | ai |
| nginx | nginx | web-servers |
| ntfy | ntfy | developer-tools |
| ollama | Ollama | ai |
| open-webui | Open WebUI | ai |
| openclaw | OpenClaw | ai |
| openclaw-browser-sandbox | OpenClaw Browser Sandbox | ai, automation |
| openlist | OpenList | storage |
| postgresql-pgvector | PostgreSQL with pgvector | databases |
| qdrant | Qdrant | databases, ai |
| searxng | SearXNG | search |
| sillytavern | SillyTavern | ai |
| umami | Umami | analytics |
| uptime-kuma | Uptime Kuma | monitoring |
| vaultwarden | Vaultwarden | security |
| wordpress | WordPress | cms |
Categories: ai, analytics, automation, cms, databases,
developer-tools, monitoring, productivity, search, security,
storage, web-servers. Deploy a template
explains how to install one by hand.
Distributions
Section titled “Distributions”Components and templates are tested on kind, with the Kubernetes minor of the RKE2 release that the Zeabur installer pins: one node for single-node, three for multi-node. They behave the same on other distributions apart from these known differences:
- k3s: run it with
--disable=traefik,metrics-serverso both come from their charts. It installs its own Gateway API CRDs. Behind NAT, leavenode-external-ipunset, or Traefik loses client addresses. - KubeSolo: its default storage class retains volumes (and their data) after
their claims are deleted. On Ubuntu, map the hostname to the node IP in
/etc/hosts; with127.0.1.1the API server refuses to proxy to the kubelet. Its node has no control-plane label. - Longhorn needs open-iscsi with
iscsidrunning, theiscsi_tcpmodule and an NFSv4 client on every node, and multipathd kept off its disks; see the comments in its file. CI installs them into kind's nodes. distributionslimits a component to some distributions (system-upgrade-controller: k3s). The kubesolo e2e profile skips a component whose list omits kubesolo.- Templates name no StorageClass, so their volumes use the cluster's default
StorageClass. Without a default, their claims stay
Pending.
Component format
Section titled “Component format”Abridged; schema/v1/component.schema.json documents every field.
apiVersion: catalog.zeabur.com/v1kind: Componentmetadata: name: Traefik Proxy description: Why it is recommended, in one or two sentences. license: MIT # SPDX; OSI-approved, or opt-in only (see below) homepage: https://traefik.io/traefik/maturity: independent # cncf-graduated | cncf-incubating | cncf-sandbox | kubernetes-sig | independentdefaults: {single-node: recommended, multi-node: recommended}requires: [certificates] # installed firstdistributions: [k3s] # optional; omitted means any cluster
releases: # installed in order - name: traefik namespace: traefik chart: oci://ghcr.io/traefik/helm/traefik # or repo: https://… plus chart: <name> version: 41.7.1 # exact; Renovate bumps it chartDigest: sha256:… # required with chart; see Digests values: | # shared by every profile gatewayClass: # Every value has a comment saying why; `catalog validate` enforces it. enabled: true image: # Every image is <tag>@sha256:<digest>; `catalog render` enforces it. tag: v3.7.16@sha256:… profiles: # merged over values like helm -f single-node: | deployment: # One node: a second replica gains nothing. replicas: 1 - name: crds namespace: kube-system urls: [https://github.com/…/releases/download/{version}/standard-install.yaml] version: v1.6.2 urlDigests: [sha256:…] # one per URL, in order # or: manifests: | objects applied as written
inputs: [] # values to ask for or generate; Secrets are created from themcontributes: [] # values added to another component's release (CrowdSec -> Traefik)detection: {...} # CRDs, API services, Helm charts, workloadssecurity: {...} # RBAC summary, cluster-wide, privileged, podSecurity, egress, credentials, tenant riskssmoke: [] # extra e2e checksleftovers: [] # what the upstream keeps on uninstall (e.g. CRDs)Digests
Section titled “Digests”Each release pins its content. The installer and catalog render stop when a
chart or URL does not match its digest.
chartDigestof an HTTP chart is thedigestof its entry in the repository'sindex.yaml, the sha256 of the.tgz.chartDigestof an OCI chart is its manifest digest, ashelm pushprints it;helm pull <chart>:<version>@sha256:…checks it.urlDigestsare the sha256 of each downloaded file.- Images go in values as
<tag>@sha256:<digest>, including images a chart passes to its operator in arguments. A chart that cannot take a digest lists its images inunpinnedImages, with a comment saying why. URL releases are exempt:urlDigestspin what the upstream wrote.
Renovate updates the tag and digest of images and OCI charts. It cannot read
the digest of an HTTP chart or a URL: after it bumps version, catalog render fails and prints the new digest to copy in.
Installs are deterministic: charts never generate secrets or certificates.
Generated values are inputs (stored in Secrets), certificates come from
cert-manager, and catalog render fails if two renders differ.
Template format
Section titled “Template format”Abridged; schema/v1/template.schema.json documents every field.
apiVersion: catalog.zeabur.com/v1kind: Templatemetadata: name: PostgreSQL description: PostgreSQL 18 managed by CloudNativePG, with Barman Cloud WAL archiving and scheduled base backups. license: PostgreSQL AND Apache-2.0 homepage: https://www.postgresql.org/ icon: https://cdn.zeabur.com/marketplace/postgresql.svg categories: [databases]
requires: [databases/cloudnative-pg]
services: # the workloads in manifests.yaml - name: postgresql securityProfile: restricted # or baseline, with baselineJustification ports: [{name: postgresql, port: 5432}] resources: {requests: {cpu: 100m, memory: 256Mi}, limits: {cpu: "1", memory: 1Gi}}
inputs: - name: backupDestination # the person installing gives it description: Dedicated S3 bucket and unique prefix for this cluster. required: true validation: {pattern: '^s3://[a-z0-9][a-z0-9.-]+/[A-Za-z0-9/_-]+$'} # From n8n: the installer generates this value into a Secret. # - name: encryptionKey # generate: random # secret: {name: n8n-secrets, key: encryption-key}
security: {...} # summary, risks with mitigations, unauthenticated servicesscalability: {...} # replica range and what blocks moreupgradeNotes: [] # what an upgrade needssmoke: [] # resource condition, then an HTTP or SQL checkmanifests.yaml allows only field substitution: {{ .Namespace }},
{{ .Domain }}, {{ .ClusterIssuer }} and {{ .Inputs.<name> }} of an input
that is not generated. It allows no conditions, functions, pipelines or trim
markers. An input that is not given takes its default, or is empty. Use an
input with a default where a template needs a choice. Rendered objects
reference Secrets and never contain generated values.
Adding a component
Section titled “Adding a component”-
Pick a capability and an upstream with an OSI-approved license; prefer CNCF or Kubernetes SIG projects. A license under
optInOnly(SSPL-1.0) is allowed only withoptionalorunsupportedin every profile. -
Create
components/<capability>/<upstream>.yamlfrom a similar file. Give both profiles values, each with a comment saying why. -
Check locally (
nix developprovides Go, Helm, kubectl, kubeconform, etcd, kube-apiserver and kind; L1 and L2 also need Docker on Linux):go run ./tools/cmd/catalog validate # schema, license, references, commentsgo run ./tools/cmd/catalog render # helm template + kubeconformgo run ./tools/cmd/catalog e2e -tier l0 # what every pull request runsgo run ./tools/cmd/catalog e2e -tier l2 <id> -
Open a pull request with signed-off commits (
git commit -s).
Adding a template
Section titled “Adding a template”-
Create
templates/<id>/template.yamlandmanifests.yamlfrom a similar template. The ID is lowercase words joined by hyphens, at most 49 characters. -
Meet the template standards: pinned images, Restricted Pod Security, an ingress NetworkPolicy for any service without its own sign-in, resource limits, backups for state, and honest scaling limits.
-
Add smoke checks that prove the application answers: a resource condition, and an HTTP or SQL check.
-
Check locally.
values.jsonis a JSON object with a value for each input the template asks for:go run ./tools/cmd/catalog validatego run ./tools/cmd/catalog template lint -values values.json \-domain app.example.test -cluster-issuer letsencrypt <id>go run ./tools/cmd/catalog e2e -tier l2 <id> -
Open a pull request with signed-off commits (
git commit -s).
Tests run in three tiers. A check that one tier leaves out runs in a later
tier. catalog e2e -tier l0|l1|l2 runs a tier as CI does. L0 needs no Docker.
L1 and L2 create kind clusters, so they need Docker on Linux; with
-cluster current they use the cluster that KUBECONFIG names instead.
L0, every pull request (ci.yml, one job):
- runs the Go tests;
- validates every file against its schema, and checks that every template's
requiresresolves on both profiles; - rejects component licenses that are not OSI-approved, except those under
optInOnlyinconfig/licenses.yamlin components no profile recommends; - renders every profile twice (output must be identical) and validates it
with kubeconform for each version in
config/kubernetes.yaml, checking custom resources against the CRDs rendered with them; - checks every template, rendered with its test inputs, against the safety rules, the upstream Pod Security checks and kubeconform;
- starts etcd and kube-apiserver from
flake.nix(the Kubernetes release that the Zeabur installer pins) with only the rendered CRDs, then sends every rendered component (both profiles) and template to the API server withkubectl apply --dry-run=server. The API server checks fields, defaults, CEL rules and Pod Security, and nothing runs. Each template namespace warns at its enforce level, and a warning fails the template.
L1, in the merge queue and after a merge to main (ci.yml): installs each
changed component and template, and each one that installs a changed
component, on single-node amd64 kind, then runs its smoke checks. Tests that
share dependencies share a cluster, and the dependencies install once per
cluster. A component is uninstalled after its smoke checks unless a later test
in the cluster needs it. A change to config/, tools/, the flake or a
workflow tests everything.
L2, every night and before each release (e2e.yml, called by
nightly.yml and release.yml): tests every component in a fresh kind
cluster for each profile (one node for single-node, three for multi-node), on
amd64 and arm64:
- installs its dependencies, then the latest release's version when it differs, then upgrades to the new version;
- measures idle memory and CPU every 10 seconds until five samples agree, for at most 3 minutes;
- runs the smoke checks, uninstalls, and fails on anything left behind.
Components listed under unready in config/e2e.yaml need credentials CI
lacks: they are installed without waiting, smoke tested and uninstalled, not
measured. L2 installs every template on single-node amd64 with the components
it requires, runs its smoke checks, then measures the idle memory and CPU of
the pods in its namespace the same way (measurements/templates/<id>.json).
It checks that the single-node
recommended set, measured on amd64, fits config/budget.yaml.
nightly.yml opens a pull request with fresh measurements/; run it by hand
to measure now. Renovate opens a pull request for every new upstream version
and image digest.
The kubesolo profile (-profile kubesolo, on a Linux host with root) installs
the KubeSolo and helm-controller releases pinned in config/kubernetes.yaml
on the host, with their checksums, and runs the L2 test for the capabilities
listed there (ingress, certificates, databases) with the single-node values.
Templates do not run on it: they need NetworkPolicy. Components whose
distributions omit kubesolo do not run either. Its measurements are keyed
kubesolo-<architecture>-<version>, beside the kind ones.
L0 starts its own API server, and L1 and L2 create their own kind clusters,
each with its own kubeconfig. Each tier deletes them (-keep keeps them) and
prints how long each phase took. With
ZEABUR_REGISTRY_MIRRORS (docker.io=http://mirror:5001,ghcr.io=...), the
nodes pull through those mirrors.
Test inputs for templates
Section titled “Test inputs for templates”Template tests take their input values from config/e2e.yaml. They need no
repository secrets, so pull requests run them in full:
templateInputs: values for every template that declares the input.templates.<id>: values for one template, objects to apply intemplates-e2e-<id>first (such as a Secret that an input names) and smoke checks to skip with the reason.
Every value is disposable test data. A value that uses .S3 starts SeaweedFS
(object-storage/seaweedfs-operator) in templates-e2e-s3, with HTTPS on TCP
443 from a test CA. Backups and object storage go to it. The Secret e2e-s3
in the template's namespace holds its credentials and the CA as ca.crt.
Templates take the CA through an optional input such as backupCASecret. A
template that cannot use a custom CA skips only that smoke check. To test
against other storage, change the template's values in config/e2e.yaml.
Releases
Section titled “Releases”Tagging vX.Y.Z publishes:
index.json: every component with its install order per profile and measurements, recommended components per capability, every template with its measurements, the budget and the Kubernetes versions;bundle.tar.gz:index.jsonpluscomponents/,templates/, schemas, config and measurements;checksums.txt, and a cosign keyless Sigstore bundle for each.
nix build .#release makes the same three files from any commit, byte for
byte, so a consumer can build an unreleased commit and later compare it with
the signed release.
cosign verify-blob index.json --bundle index.json.sigstore.json \ --certificate-identity-regexp '^https://github.com/zeabur-oss/catalog/.github/workflows/release.yml@refs/tags/v' \ --certificate-oidc-issuer https://token.actions.githubusercontent.comConsumers pin a release, verify it, and read index.json. indexVersion
changes only on incompatible changes. A typical consumer runs detection,
offers the recommended components for the cluster's profile, asks for their
inputs, and generates installs in installOrder, merging contributes into
their targets. For a template, it checks that the cluster has everything in
requires, asks for the template's inputs, generates the others into Secrets,
and applies manifests.yaml rendered for the project's namespace.
License
Section titled “License”The tools and original files are licensed under Apache-2.0. Each upstream,
application, image and chart keeps its own license, named in
metadata.license. Some use source-available licenses such as BUSL-1.1 or
SSPL; those components are opt-in only, and those templates name and link the
license in their description. Contributions use the
Developer Certificate of Origin; see
CONTRIBUTING.md.