Skip to main content

Overview

pgvector adds a vector column type and approximate nearest-neighbor indexes to PostgreSQL, so embeddings live in the same database as the relational data they describe. This template deploys one PostgreSQL 18 server with pgvector 0.8.6 on a persistent volume, an optional PgBouncer connection pooler in front of it, and an optional cron job that writes compressed pg_dumpall backups to AWS S3, Google Cloud Storage or a MinIO (S3-compatible) bucket. The extension is not just installed in the image — the template creates it on first boot, in the database named in your credentials secret. A fresh install can store vectors and run similarity queries with no setup SQL. Database credentials are not template values. The server reads its username, password and database name from a dictionary secret you create before installing, so no password passes through Helm or lands in the release. The template creates no credential secret of its own.
This template deploys into an existing GVC that you already have. It does not create or manage a GVC.
This is a single server, not a cluster — there is no replication or failover. For an automatically failing-over cluster use PostgreSQL Highly Available, whose image also carries pgvector; see Scaling and Availability for the version difference.

What Gets Created

Prerequisites

One secret must exist before you install. It holds the credentials your applications put in their connection strings. Secrets are org-level, so no GVC flag is involved.
1

Create the database credentials secret

A dictionary secret holding exactly three keys — username, password and database. PostgreSQL creates that user (as a superuser) and that database the first time the volume is initialized, and the template creates the vector extension inside it:
Set config.credentialsSecretName to this name. Read it back later with cpln secret reveal SECRET_NAME -o yaml — a bare cpln secret reveal prints only a summary table, not the values.
Create the secret before installing, or the deployment wedges silently. The template refuses to render when config.credentialsSecretName is blank, but a name that points at a secret which does not exist installs successfully and then never starts. The container never runs, so cpln logs returns zero lines. The one place the reason appears is status.versions[].message:
Use get-deployments — plain cpln workload get has no versions key. Creating the missing secret repairs the deployment on its own within several minutes, or run cpln workload force-redeployment RELEASE_NAME-pgvector --gvc GVC_NAME to skip the wait. First boot then runs normally and the vector extension is created.
Backups are optional and need a bucket plus, for AWS or GCP, a Control Plane cloud account — see Backing Up. MinIO backups need a second dictionary secret holding accessKey and secretKey. Nothing else is required for a default install.

Installation

Install the chart from the marketplace registry, pointing it at the secret you created:
Or follow the instructions for your preferred method:

UI

Browse, install, and manage templates visually

CLI

Manage templates from your terminal

Terraform

Declare templates in your Terraform configurations

Pulumi

Declare templates in your Pulumi programs

Configuration

Each block below is the shipped default for that part of values.yaml.

Server and Resources

On a stateful workload the ratio of maxCpu to minCpu may not exceed 4:1. Raising maxCpu without raising minCpu can cross that limit and is rejected when the workload is applied.

Credentials and Extensions

The three credential keys are applied only when the volume is first initialized. Changing the secret afterwards does not change the stored password — see Rotating the Password. config.extraExtensions lists additional extensions to create alongside vector at first boot, such as pg_trgm, pgcrypto or btree_gin. Each entry must be an extension the image already ships and must match ^[a-z][a-z0-9_]*$; anything else is refused when the chart renders. The vector extension is always created and does not need to be listed.
A name the image does not carry costs you that extension. The first-boot script stops at the bad name, the container exits once, and the restart finds a populated data directory and skips initialization — leaving a healthy server that has vector (created first) but not your extra extension, with nothing in the deployment status to say so. Adding an extension later is one statement:

Storage

Internal Access

The same setting applies to the PgBouncer workload when it is enabled. A change to internalAccess can take several minutes to take effect — re-test before concluding it did not apply.

Public Access

When enabled, Control Plane assigns a canonical endpoint. Read it from status.canonicalEndpoint:
Public connections are unencrypted. The image ships no TLS certificate, so a client that demands sslmode=require cannot connect; the libpq default of sslmode=prefer falls back to plaintext. Anyone who reaches the endpoint needs only the database password, so prefer internal access over RELEASE_NAME-pgvector.GVC_NAME.cpln.local where you can. A change to publicAccess can take several minutes to take effect.

PgBouncer

PgBouncer multiplexes many application connections into a small pool of real database connections — the bursty, short-lived connections typical of retrieval and embedding services. When enabled it becomes the endpoint your applications connect to, and it reads the same credentials secret and identity as the server — nothing extra to configure.
In transaction mode a plain SET hnsw.ef_search is not bound to your client — it can leak between clients. PgBouncer hands the same server connection to different clients, so your setting may be lost, may be replaced by another client’s value, or may raise ERROR: unrecognized configuration parameter "hnsw.ef_search". It matters on this template because ef_search is the recall knob. Either scope the setting to a transaction with SET LOCAL (see Using pgvector), or set pgbouncer.poolMode: session at the cost of connection multiplexing. This is upstream PgBouncer behaviour; PgBouncer is off by default, so a default install is not exposed to it.

Backup

A cron workload runs pg_dumpall on the schedule and uploads a gzip-compressed SQL dump of every database and role to the bucket for the chosen provider. It authenticates with the username and password from your credentials secret. Each provider’s bucket, cloud account and policy steps are under Backing Up.
Changing backup.provider on an existing release leaves the old cloud binding attached. Control Plane merges an identity’s cloud-binding block and never removes one, so a release switched from aws to gcp keeps both bindings even though the template renders only the new one. Uninstall and reinstall to change providers cleanly.

Connecting

Everything below is reachable from inside the GVC, subject to internalAccess.type; the public path exists only when publicAccess.enabled: true. To verify from your own machine, tunnel to the server and confirm the extension is present:
Or run psql inside the container, where the credentials are already present as environment variables:

Using pgvector

The vector extension is already created in the database named in your credentials secret, so there is no setup SQL to run:
Both index types are available; the planner chooses them on cost. Approximate indexes trade recall for speed, so an exact match can be missed at the default settings — raise the recall knob when results look incomplete. Large index builds are much faster when the graph fits in maintenance_work_mem, for example SET maintenance_work_mem = '512MB'; within the resources.maxMemory you have given the server. With PgBouncer in transaction mode, set these per query inside a transaction so the setting cannot leak to another client:
A vector column holds up to 16,000 dimensions but can only be indexed up to 2,000 — 1,536-dimension embeddings index fine, while 3,072-dimension ones are rejected with column cannot have more than 2000 dimensions for hnsw index. Use halfvec, which indexes up to 4,000 dimensions, or reduce the dimensionality.

Operations

Backing Up

Backups are off by default. Enable them with backup.enabled: true, pick a backup.provider, and complete that provider’s setup below first. The job writes postgres-TIMESTAMP.sql.gz objects under backup.PROVIDER.prefix in your bucket on every run of backup.schedule.

Backup Prerequisites

Complete the steps for your provider before enabling backups.

AWS S3

  1. Create an S3 bucket. Set backup.aws.bucket to its name and backup.aws.region to its region.
  2. If you do not have one yet, create a Control Plane cloud account for the AWS account holding the bucket. Set backup.aws.cloudAccountName to its name.
  3. Create an IAM policy scoped to exactly that bucket, replacing YOUR_BUCKET_NAME:
  1. Set backup.aws.policyName to that policy’s name. The template attaches it to the workload identity and nothing else — the bucket in your policy is the only storage the backup job can reach.

GCS

  1. Create a GCS bucket. Set backup.gcp.bucket to its name.
  2. Create a Control Plane cloud account for the GCP project holding the bucket, and set backup.gcp.cloudAccountName to its name.
  3. Grant the cloud account’s service account the Storage Admin (roles/storage.admin) role. The template additionally binds the identity to roles/storage.objectAdmin on exactly the bucket in backup.gcp.bucket.

MinIO

No cloud account is needed — the job authenticates with a second prerequisite secret.
  1. Create the bucket in MinIO. Set backup.minio.bucket to its name.
  2. Set backup.minio.endpoint to the S3 API address including the port. For the MinIO template deployed in the same GVC that is http://WORKLOAD_NAME.GVC_NAME.cpln.local:9000.
  3. Create a dictionary secret holding exactly the keys accessKey and secretKey, and set backup.minio.credentialsSecretName to its name. For the MinIO template these are its admin username and password:
The policy this template creates grants the identity reveal on this secret as well as the database credentials secret and the first-boot SQL secret, and on nothing else.

Restoring a Backup

Each backup is a gzip-compressed pg_dumpall script, so it is restored by connecting to the postgres maintenance database as the superuser from your credentials secret; the script recreates the roles and databases it contains. Because the dump includes CREATE EXTENSION IF NOT EXISTS vector, restore into a server whose image carries pgvector — a fresh install of this template, for example. Replaying it into a stock postgres:18 fails at that line and the tables are never created. Run the commands from a client that can reach both the bucket and the server — a workload inside the GVC, or your own machine with cpln port-forward RELEASE_NAME-pgvector 5432:5432 --gvc GVC_NAME open and --host=127.0.0.1 in place of the internal hostname.
This procedure is derived from the backup image’s script and the dump format it produces; it has not been exercised against a live install of this template. Rehearse a backup and restore before relying on it.
already exists errors for the role, the database and the vector extension that the server created at first boot are expected; the rest of the script loads normally. Restore into an empty database (a fresh install, or one whose tables you have dropped) to avoid duplicate-object errors on existing tables. Use a PostgreSQL 18 psql — an older client prints invalid command \unrestrict at the end of a PostgreSQL 18 dump.

Rotating the Password

The server reads the secret only when the volume is first initialized, so change the password inside PostgreSQL first, then update the secret to match:
Workloads resolve the secret when a replica starts, so an updated secret is not picked up by a running replica. The backup job reads it fresh on its next scheduled run; if PgBouncer is enabled, force a redeployment so it reconnects with the new password:

Scaling and Availability

  • The server is pinned to one replica. Do not raise it — a second stateful replica gets its own volume, which is a second empty database rather than a replica.
  • PgBouncer is stateless; pgbouncer.replicas scales it horizontally for high connection counts.
  • Every cpln helm upgrade restarts the server, including the first upgrade after an install even when nothing changed. Plan each one as a short write outage. Data on the volume survives, including built HNSW graphs, which are not rebuilt.
  • A node failure reschedules the server and reattaches the same volume. The exposure is minutes of downtime, not data loss.
  • For automatic failover use PostgreSQL Highly Available, whose image also carries pgvector — but not the same build. Both hnsw and ivfflat exist there, so the gap is fixes and refinements rather than a missing index type; verify against your own queries before treating the two as interchangeable.

Troubleshooting

Symptom: the install succeeds, the workload stays not ready indefinitely, and cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-pgvector"}' --limit 50 --since 10m returns zero lines.Cause: config.credentialsSecretName names a secret that does not exist, so the container is never started. The message is only visible in status.versions[].message:
Fix: create the secret as in Prerequisites. The deployment recovers on its own within several minutes, or run cpln workload force-redeployment RELEASE_NAME-pgvector --gvc GVC_NAME.
Symptom: cpln helm install or upgrade stops before touching any resource:
Cause: an entry in config.extraExtensions contains a hyphen, a capital letter or other punctuation. Entries are interpolated into SQL, so the chart only accepts bare lower-case identifiers.Fix: use the extension’s exact name as PostgreSQL knows it — pg_trgm, not pg-trgm.
Symptom: vector exists but CREATE EXTENSION for one of your extra extensions never ran, and the first-boot log shows ERROR: extension "NAME" is not available followed by a restart.Cause: the name is valid but the image does not ship that extension. The first-boot script stopped at that line; the restart found an initialized data directory and skipped initialization, so the remaining statements never ran.Fix: create the extensions the image does carry by hand with CREATE EXTENSION IF NOT EXISTS NAME;, and remove the unavailable one from config.extraExtensions. To start over with a corrected list, uninstall (which deletes the volume set) and reinstall.
Symptom: through RELEASE_NAME-pgbouncer a plain SET hnsw.ef_search = ... sometimes errors with unrecognized configuration parameter, or SHOW hnsw.ef_search returns a value you did not set. Direct connections to RELEASE_NAME-pgvector behave normally.Cause: pgbouncer.poolMode: transaction hands the same server connection to different clients, so a session-level SET is not bound to the client that issued it.Fix: scope the setting with SET LOCAL inside a transaction (see Using pgvector), or set pgbouncer.poolMode: session.
Symptom: creating an hnsw (or ivfflat) index on a vector(N) column with N above 2,000 fails with this error, although inserting the vectors worked.Cause: a vector column stores up to 16,000 dimensions but pgvector indexes it only up to 2,000.Fix: store the embeddings as halfvec(N), which indexes up to 4,000 dimensions, or reduce the embedding dimensionality.
Symptom: a connection string carrying sslmode=require (or verify-ca / verify-full) cannot connect to the canonical endpoint, while the same credentials work without it.Cause: the image ships no TLS certificate, so the server does not offer SSL.Fix: use the libpq default sslmode=prefer, or sslmode=disable, and prefer the internal address RELEASE_NAME-pgvector.GVC_NAME.cpln.local over the public endpoint wherever possible.
Symptom: an install or upgrade fails and the server workload is not created or updated.Cause: resources.maxCpu is more than four times resources.minCpu, which the platform rejects on a stateful workload.Fix: raise resources.minCpu or lower resources.maxCpu so the ratio is at most 4:1.

Important Notes

  • Create the credentials secret before installing. A reference to a secret that does not exist wedges the workload with no log output; see Prerequisites for the one command that shows the reason.
  • The vector extension is created by this template, not by the image, in the database named in your credentials secret, on first boot only.
  • Credentials and config.extraExtensions are read only when the volume is first initialized. Add an extension later with CREATE EXTENSION IF NOT EXISTS ...; rotate a password with ALTER ROLE, then update the secret — see Rotating the Password.
  • PgBouncer in transaction mode leaks session settings between clients, including hnsw.ef_search. Use SET LOCAL inside a transaction, or poolMode: session.
  • publicAccess is unencrypted — the image ships no TLS certificate, so sslmode=require fails. Prefer internal access.
  • Do not scale the server past one replica. It is a single instance on a single volume, not a replicated cluster.
  • Every cpln helm upgrade restarts the server. Treat each one as a short planned write outage.
  • Restore with a PostgreSQL 18 psql, into a server that carries pgvector. A stock postgres:18 fails on the dump’s CREATE EXTENSION line.
  • cpln helm uninstall deletes the volume set and the database with it. Your credentials secret is yours and is left alone.

External References

pgvector Documentation

Upstream reference for vector types, operators, and index tuning

pgvector Image

The image this template deploys, and its available tags

PostgreSQL 18 Documentation

Official documentation for the server version shipped here

PgBouncer Documentation

PgBouncer configuration reference

pgvector Template

Source files, default values, and chart definition