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)
  • A PostgreSQL 14 or later instance reachable from the cluster, with a database and user already created for the Registry Server

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.

The steps below build a values file, create a Secret holding the database password, install the chart, and route traffic to the resulting Service.

Create a values file

The chart's config block maps directly to the Registry Server's configuration file, so any valid configuration field can be set under config. Start with a minimal file that points the server at a catalog source and your PostgreSQL instance:

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

This configuration serves a single registry with anonymous access. Replace host, port, database, and user with the values for your own PostgreSQL instance. The pgpass entry in the next step must repeat those same four values, because libpq matches them field by field when it looks up the password.

See Configure sources and registries for the full field reference, and Set up authentication to replace mode: anonymous before exposing the server.

Provide database credentials

Keep the database password out of your values file and supply it through a pgpass file instead. 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.

Create the release namespace and the Secret. The pgpass format is positional, so the first four fields must match config.database in your values file exactly:

kubectl create namespace toolhive-system

kubectl create secret generic registry-pgpass \
--namespace toolhive-system \
--from-literal=.pgpass='<HOST>:<PORT>:<DATABASE>:<USER>:<PASSWORD>'

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.

Add the following to your values file:

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.

For separate migration and application users, TLS verification, and alternatives to pgpass such as environment variables or AWS RDS IAM, see Database configuration.

Install the chart

With values.yaml complete, 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 \
-f values.yaml \
--wait --timeout=3m

Confirm that the pod is ready:

kubectl get pods -n toolhive-system \
-l app.kubernetes.io/instance=registry-server

Expose the Registry Server

The chart creates a ClusterIP Service on port 8080, named after the release and chart in the form <RELEASE_NAME>-toolhive-registry-server. For the registry-server release above, that's registry-server-toolhive-registry-server. Confirm the name:

kubectl get svc -n toolhive-system \
-l app.kubernetes.io/instance=registry-server

Set fullnameOverride in your values file if you want a shorter, stable name to reference from routing resources.

The chart doesn't create an Ingress, HTTPRoute, or any other external entry point, so route to the Service with whatever your cluster already uses:

  • Ingress controller: create an Ingress with the Service as its backend on port 8080.
  • Gateway API: create an HTTPRoute with the Service as its backendRef.
  • Cloud load balancer: change the Service type in your values file and add your provider's annotations under service.annotations.
values.yaml (excerpt)
service:
type: LoadBalancer
annotations: {}

Whichever you choose, set up authentication before the server becomes reachable outside the cluster. The values file above sets mode: anonymous, which lets any caller read the registry.

To check the API before you wire up routing, port-forward the Service. This command blocks, so leave it running and open a separate terminal for the request:

kubectl port-forward -n toolhive-system \
svc/registry-server-toolhive-registry-server 8080:8080

In the separate terminal, query one of the registries you defined in config:

curl -s http://localhost:8080/registry/default/v0.1/servers | jq .

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.

Next steps