跳到內容
開源

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.

components/<capability>/<upstream>.yaml one file per capability × upstream
templates/<id>/template.yaml one application: inputs, services, security, scaling, smoke checks
templates/<id>/manifests.yaml its Kubernetes objects
schema/v1/ JSON Schemas for both
config/licenses.yaml OSI-approved SPDX identifiers; opt-in-only exceptions
config/kubernetes.yaml Kubernetes versions; kind node version for e2e
config/budget.yaml single-node memory budget (MiB)
config/e2e.yaml test-only values
measurements/ idle memory and CPU, written by CI
tools/ the `catalog` command (Go)
docs/ template standards, deploying a template

Both kinds of file share one envelope:

apiVersion: catalog.zeabur.com/v1
kind: Component # or Template
metadata:
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.

CapabilityUpstreamsingle-nodemulti-node
ingressTraefikrecommendedrecommended
certificatescert-managerrecommendedrecommended
observability-operatorVictoriaMetrics operatorrecommendedrecommended
metricsVictoriaMetrics (k8s stack)recommendedrecommended
metricsmetrics-serverrecommendedrecommended
logsVictoriaLogs (via the operator)recommendedrecommended
node-updatessystem-upgrade-controller (k3s only)recommendedrecommended
intrusion-preventionCrowdSecoptionaloptional
volume-backupK8upoptionaloptional
volume-backupVelero (administrators only)optionaloptional
event-autoscalingKEDAoptionalrecommended
replicated-storageLonghorn (default storage class on multi-node)optionalrecommended
event-autoscalingKEDA HTTP add-on (beta)optionaloptional
uptime-probesPrometheus blackbox exporteroptionalrecommended
runtime-securityFalcooptionaloptional
vulnerability-scanningTrivy Operatoroptionaloptional
resource-recommendationsVertical Pod Autoscaler (recommender)optionaloptional
remote-accessTailscale Kubernetes operatoroptionaloptional
config-reloadStakater Reloaderoptionaloptional
external-secretsExternal Secrets Operator (no SecretStore by default)optionaloptional
databasesCloudNativePG with the Barman Cloud plugin (PostgreSQL)optionaloptional
databasesPercona Operator for MongoDB (server SSPL-1.0, opt-in only)optionaloptional
databasesMariaDB Operator (MySQL-compatible)optionaloptional
databasesDragonfly Operator (Redis-compatible; server BUSL-1.1, opt-in only)optionaloptional
databasesMOCO (MySQL 8)optionaloptional
object-storageSeaweedFS operator (beta)optionaloptional
container-registryzotoptionaloptional
image-updatesKeeloptionaloptional

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.

IDApplicationCategories
browserlessBrowserlessautomation
cliproxyapiCLIProxyAPIai
difyDifyai
error-pagesError pagesweb-servers
ghostGhostcms
giteaGiteadeveloper-tools
haloHalocms
hermes-agentHermes Agentai, automation
librechatLibreChatai
litellmLiteLLMai
lobechatLobeChatai
meilisearchMeilisearchsearch
memosMemosproductivity
minifluxMinifluxproductivity
n8nn8n PostgreSQLautomation
n8n-ai-brainn8n (AI Brain)ai, automation
n8n-sqliten8n SQLiteautomation
new-apiNew APIai
nginxnginxweb-servers
ntfyntfydeveloper-tools
ollamaOllamaai
open-webuiOpen WebUIai
openclawOpenClawai
openclaw-browser-sandboxOpenClaw Browser Sandboxai, automation
openlistOpenListstorage
postgresql-pgvectorPostgreSQL with pgvectordatabases
qdrantQdrantdatabases, ai
searxngSearXNGsearch
sillytavernSillyTavernai
umamiUmamianalytics
uptime-kumaUptime Kumamonitoring
vaultwardenVaultwardensecurity
wordpressWordPresscms

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.

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-server so both come from their charts. It installs its own Gateway API CRDs. Behind NAT, leave node-external-ip unset, 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; with 127.0.1.1 the API server refuses to proxy to the kubelet. Its node has no control-plane label.
  • Longhorn needs open-iscsi with iscsid running, the iscsi_tcp module 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.
  • distributions limits 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.

Abridged; schema/v1/component.schema.json documents every field.

apiVersion: catalog.zeabur.com/v1
kind: Component
metadata:
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 | independent
defaults: {single-node: recommended, multi-node: recommended}
requires: [certificates] # installed first
distributions: [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 them
contributes: [] # values added to another component's release (CrowdSec -> Traefik)
detection: {...} # CRDs, API services, Helm charts, workloads
security: {...} # RBAC summary, cluster-wide, privileged, podSecurity, egress, credentials, tenant risks
smoke: [] # extra e2e checks
leftovers: [] # what the upstream keeps on uninstall (e.g. CRDs)

Each release pins its content. The installer and catalog render stop when a chart or URL does not match its digest.

  • chartDigest of an HTTP chart is the digest of its entry in the repository's index.yaml, the sha256 of the .tgz.
  • chartDigest of an OCI chart is its manifest digest, as helm push prints it; helm pull <chart>:<version>@sha256:… checks it.
  • urlDigests are 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 in unpinnedImages, with a comment saying why. URL releases are exempt: urlDigests pin 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.

Abridged; schema/v1/template.schema.json documents every field.

apiVersion: catalog.zeabur.com/v1
kind: Template
metadata:
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 services
scalability: {...} # replica range and what blocks more
upgradeNotes: [] # what an upgrade needs
smoke: [] # resource condition, then an HTTP or SQL check

manifests.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.

  1. 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 with optional or unsupported in every profile.

  2. Create components/<capability>/<upstream>.yaml from a similar file. Give both profiles values, each with a comment saying why.

  3. Check locally (nix develop provides 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, comments
    go run ./tools/cmd/catalog render # helm template + kubeconform
    go run ./tools/cmd/catalog e2e -tier l0 # what every pull request runs
    go run ./tools/cmd/catalog e2e -tier l2 <id>
  4. Open a pull request with signed-off commits (git commit -s).

  1. Create templates/<id>/template.yaml and manifests.yaml from a similar template. The ID is lowercase words joined by hyphens, at most 49 characters.

  2. 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.

  3. Add smoke checks that prove the application answers: a resource condition, and an HTTP or SQL check.

  4. Check locally. values.json is a JSON object with a value for each input the template asks for:

    go run ./tools/cmd/catalog validate
    go 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>
  5. 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):

  1. runs the Go tests;
  2. validates every file against its schema, and checks that every template's requires resolves on both profiles;
  3. rejects component licenses that are not OSI-approved, except those under optInOnly in config/licenses.yaml in components no profile recommends;
  4. 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;
  5. checks every template, rendered with its test inputs, against the safety rules, the upstream Pod Security checks and kubeconform;
  6. 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 with kubectl 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:

  1. installs its dependencies, then the latest release's version when it differs, then upgrades to the new version;
  2. measures idle memory and CPU every 10 seconds until five samples agree, for at most 3 minutes;
  3. 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.

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 in templates-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.

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.json plus components/, 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.com

Consumers 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.

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.