Skip to main content

Deploy with Helm

info

Helm is the recommended way to deploy the Registry Server. For an alternative approach, see Deploy manually. The ToolHive Operator method is deprecated; if you're currently using it, see Migrate from the operator to Helm.

Prerequisites

  • A Kubernetes cluster (current and two previous minor versions are supported)
  • Permissions to create resources in the cluster
  • kubectl configured to communicate with your cluster
  • Helm v3.10 or later (v3.14+ is recommended)
  • PostgreSQL 14 or later

Overview

The official Helm chart in the toolhive-registry-server repository deploys a standalone Registry Server. Use this method when you want to manage the Registry Server like any other Helm release without installing the ToolHive Operator.

Install the chart

Install the chart from its OCI registry into the toolhive-system namespace:

helm upgrade --install registry-server \
oci://ghcr.io/stacklok/toolhive-registry-server \
-n toolhive-system --create-namespace \
-f values.yaml

Configure the Registry Server

The chart's config block maps directly to the Registry Server's configuration file. Any valid configuration field can be set under config in your values file:

values.yaml
config:
sources:
- name: toolhive
git:
repository: https://github.com/stacklok/toolhive-catalog.git
branch: main
path: pkg/catalog/toolhive/data/registry-upstream.json
syncPolicy:
interval: '30m'
registries:
- name: default
sources: ['toolhive']
auth:
mode: anonymous
database:
host: postgres
port: 5432
user: registry
database: registry
sslMode: require

Share a database host across subcharts

When the Registry Server chart runs as a subchart of an umbrella chart whose bundled services share a single PostgreSQL instance, you can set the database host once at the umbrella level under global.postgres instead of repeating it per subchart. This is a standard Helm global.* mechanism, so any umbrella chart can use it:

values.yaml (umbrella excerpt)
global:
postgres:
host: 'postgres.example.com'
port: 5432
sslMode: 'require'

When config.database.host on the Registry Server subchart is empty, the chart falls back to global.postgres.{host,port,sslMode} (port defaults to 5432). A locally set config.database.host always wins, so per-subchart overrides keep working. Only host, port, and sslMode are inherited. The user, database, and credentials still go in the subchart's own config.database block.

Stacklok Enterprise

If you run the full Stacklok Enterprise platform, its canonical umbrella chart already wires up global.postgres, so you set the shared database host once at the umbrella level rather than per subchart. See the platform-wide PostgreSQL defaults for the credential Secrets and the other components that read global.postgres.

Provide database credentials

Database credentials use the pgpass file pattern. Create a Kubernetes Secret with a pgpass-formatted entry under the key .pgpass, then point the chart at it with an init container that prepares the file and a PGPASSFILE environment variable that tells libpq where to find it.

The chart's main container runs with readOnlyRootFilesystem: true as the non-root UID 65535, so the init container copies the Secret into an emptyDir volume and applies 0600 permissions there (libpq rejects pgpass files with wider permissions). Leave the pgpass-secret volume at its default mode: Kubernetes mounts Secret files owned by root, so a restrictive defaultMode such as 0600 would make the file unreadable by UID 65535 and the init container would fail to copy it.

values.yaml (excerpt)
extraEnv:
- name: PGPASSFILE
value: /pgpass-prepared/.pgpass

extraVolumes:
- name: pgpass-secret
secret:
secretName: registry-pgpass
- name: pgpass-prepared
emptyDir: {}

extraVolumeMounts:
- name: pgpass-prepared
mountPath: /pgpass-prepared
readOnly: true

initContainers:
- name: pgpass-init
image: cgr.dev/chainguard/busybox:latest
command:
- sh
- -c
- cp /pgpass-secret/.pgpass /pgpass-prepared/.pgpass && chmod 600
/pgpass-prepared/.pgpass
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65535
volumeMounts:
- name: pgpass-secret
mountPath: /pgpass-secret
readOnly: true
- name: pgpass-prepared
mountPath: /pgpass-prepared

The init container runs as the same UID as the main container, so the copied file is already owned by 65535 and the Registry Server can read it without a separate chown step. This matches the commented pgpass example in the chart's own values.yaml.

Next steps