Deploy with Helm
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
kubectlconfigured 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:
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:
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.
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:
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.
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
- Configure sources and registries to set up your data sources and sync policies
- Set up authentication to secure access to your registry
- Configure telemetry to monitor your deployment
Related information
- Database configuration for the pgpass format and user privileges
- toolhive-registry-server repository for the full set of chart values and their defaults
- Quickstart: Registry Server for a hands-on walkthrough of a minimal local deployment