Enterprise Not open source: This functionality is only available commercially.

Install Vespa on Kubernetes

The Vespa Operator should be installed using the official Helm chart. It depends on the installation of the VespaSet Custom Resource Definition (CRD), which is defined at the Kubernetes cluster scope. Through the Helm Chart, the installation of the CRD and the required RBAC permissions can be simplified. The required permissions are listed in the Permissions section.

Our container registry is located at images.ves.pa. For accessing the required Vespa on Kubernetes container images (and helm chart) you will need to contact us through our support portal. We will provide you with the authentication id and token.

Important: For production use, we require mirroring these images into your own registry or a well-known internal repository appropriate for your infrastructure!

Our support team will provide you with credentials to access the Vespa on Kubernetes image, Vespa Operator image, and the Helm Chart. They will be referred to as shown below throughout the installation steps.

# The Vespa on Kubernetes Image
export $OCI_IMAGE_REFERENCE=images.ves.pa/kubernetes/vespa

# The Vespa Operator Image
export $OCI_IMAGE_REFERENCE_OPERATOR=images.ves.pa/kubernetes/operator

# The Official Helm Chart
export $HELM_OCI_CHART_REFERENCE=oci://images.ves.pa/helm/vespa-operator

The Vespa Operator uses the Vespa Version release semantics. We encourage to use the latest release during installation.

# Vespa Version
export VESPA_VESRION=$VESPA_VERSION

Additionally, the Vespa Operator and all Vespa components are completely scoped to a namespace. We encourage choosing one to your liking.

# Set the namespace
export NAMESPACE=test

Requirements

The following tools are encouraged for a smooth deployment.

These instructions assume that your kubeconfig is pointing to an active Kubernetes cluster. Refer to the Getting Started guide to create a Kubernetes cluster. For instructions on creating a local development environment, refer to the MiniKube Setup section.

Deploy Vespa Operator

The Helm Chart installs the Vespa Operator, Role, RoleBinding, and ServiceAccount resources with the permissions to operate Vespa. Optionally, the CRD specification can be installed onto the Kubernetes cluster.

First, authenticate to the Helm Chart OCI registry. The credentials will be provided by our support team.

helm registry login images.ves.pa -u $USER-p $TOKEN

Now, an installation can be performed as follows. This will deploy the Vespa Operator to the target namespace and apply the VespaSet CRD specification to the Kubernetes cluster. Set image.repository to the Vespa On Kubernetes Image provided by our support team. The image.tag refers to the Vespa Version to deploy.

$ helm install vespa-operator $HELM_OCI_CHART_REFERENCE --namespace $NAMESPACE --create-namespace --set image.repository=$OCI_IMAGE_REFERENCE_OPERATOR --set image.tag=$VESPA_VERSION

If CRDs are managed separately, its installation can be disabled. However, the CRD specification must be manually applied to the Kubernetes cluster before installing the Helm Chart. Our support team can provide this specification if necessary.

$ kubectl apply vespasets.k8s.ai.vespa-v1.yaml
$ helm install vespa-operator $HELM_OCI_CHART_REFERENCE --namespace $NAMESPACE --create-namespace --skip-crds --set image.repository $OCI_IMAGE_REFERENCE_OPERATOR --set image.tag $VESPA_VERSION

Ensure that the Deployment was successfully applied and the operator Pod was created. It can be done using the following check.

$ kubectl wait --for=condition=available deployment/vespa-operator --timeout=120s -n $NAMESPACE \
&& kubectl get pods -l app=vespa-operator -o wide -n $NAMESPACE

Deploy a VespaSet

A VespaSet represents a quorum of ConfigServers that manage Vespa applications. Several examples of VespaSet specifications are provided in the Helm Chart samples directory. A sample VespaSet for Amazon Elastic Kubernetes Service (EKS) is shown below.

# vespaset sample for EKS
$ cat > vespaset.yaml <<EOF
apiVersion: k8s.ai.vespa/v1
kind: VespaSet
metadata:
  name: vespaset-sample
  namespace: ${NAMESPACE}
spec:
  version: "${VESPA_VERSION}"

  configServer:
    image: "${OCI_IMAGE_REFERENCE}"
    storageClass: "gp3"
    generateRbac: false

  application:
    image: "${OCI_IMAGE_REFERENCE}"
    storageClass: "gp3"

  ingress:
    endpointType: "LOAD_BALANCER"
EOF

$ kubectl apply -f vespaset.yaml

An example for a local deployment on MiniKube would be as follows.

# vespaset sample for MiniKube
$ cat > vespaset.yaml <<EOF
apiVersion: k8s.ai.vespa/v1
kind: VespaSet
metadata:
  name: vespaset-sample
  namespace: ${NAMESPACE}
spec:
  version: "${VESPA_VERSION}"

  configServer:
    image: "${OCI_IMAGE_REFERENCE}"
    storageClass: "local-storage"
    generateRbac: false

  application:
    image: "${OCI_IMAGE_REFERENCE}"
    storageClass: "local-storage"

  ingress:
    endpointType: "NONE"
EOF

$ kubectl apply -f vespaset.yaml

Note that the $OCI_IMAGE_REFERENCE is shared between the ConfigServer and the Vespa Application Pods.

Once the VespaSet is applied, the operator will automatically detect the newly applied VespaSet and create a quorum of ConfigServers.

The ConfigServers will then bootstrap the Vespa infrastructure. This process takes roughly a minute. The bootstrap process is completed once the VespaSet shows the status as RUNNING for all ConfigServer Pods.

$ kubectl describe vespaset vespaset-sample -n $NAMESPACE
Name:         vespaset-sample
Namespace:    $NAMESPACE
API Version:  k8s.ai.vespa/v1
Kind:         VespaSet
Spec:
  Application:
    Image:          192.168.49.2:5000/localhost/vespaai/kubernetes
    Storage Class:  gp3
  Config Server:
    Generate Rbac:    false
    Image:            192.168.49.2:5000/localhost/vespaai/kubernetes
    Storage Class:    gp3
  Ingress:
    Endpoint Type:  NONE
  Version:          8.643.16
Status:
  Bootstrap Status:
    Pods:
      cfg-0:
        Last Updated:  2026-01-29T21:38:45Z
        Message:       Pod is running
        Phase:         RUNNING
        Converged Version: 8.643.16
      cfg-1:
        Last Updated:  2026-01-29T21:38:09Z
        Message:       Pod is running
        Phase:         RUNNING
        Converged Version: 8.643.16
      cfg-2:
        Last Updated:  2026-01-29T21:36:32Z
        Message:       Pod is running
        Phase:         RUNNING
        Converged Version: 8.643.16
  Last Transition Time:  2026-01-29T21:33:55Z
  Message:               All configservers running
  Phase:                 RUNNING
Events:                  <none>

Deploy a Vespa Application

A Vespa application can be deployed once the bootstrap process has completed. Refer to the Vespa Sample Applications to get started. In the following example, we will use the Album Recommendation sample.

Set up the Vespa CLI to download the Album Recommendation sample application to a working directory.

$ vespa clone album-recommendation myapp && cd myapp

Modify the sample application package with resource specifications and ensure the correct Pod count, as shown below:

<?xml version="1.0" encoding="utf-8" ?>
<services version="1.0" xmlns:deploy="vespa" xmlns:preprocess="properties">

    <container id="default" version="1.0">
        <document-api/>
        <search/>

        <nodes count="2">
            <resources vcpu="2" memory="2Gb" disk="20Gb" />
        </nodes>
    </container>

    <content id="music" version="1.0">
        <min-redundancy>2</min-redundancy>
        <documents>
            <document type="music" mode="index" />
        </documents>
        <nodes count="2">
            <resources vcpu="2" memory="2Gb" disk="20Gb" />
        </nodes>
    </content>

</services>

Enable port-forwarding from the ConfigServer's ingress port 19071 to your local port 19071.

$ vespa config set target local
$ kubectl -n $NAMESPACE port-forward pod/cfg-0 19071:19071

Deploy and activate the application using the sequence below.

$ vespa prepare --target local
$ while not vespa --target local activate

The ConfigServer quorum will create the Container, Content, and Cluster-Controller Pods as specified in the application package. The deployment is considered complete once all Pods show the phase RUNNING in the VespaSet status.

Port-forwarding provides a simple way to access the ingress ports locally. For other ingress options, see the Configuring the External Access Layer section.

Feed Documents

Feed documents to the Dataplane entrypoint by port-forwarding the Dataplane ingress port and the ConfigServer ingress port.

# Ensure the port-forward to 19071 is still active
$ kubectl -n $NAMESPACE port-forward pod/cfg-0 19071:19071

# Port-forward to the dataplane ingress port
$ kubectl -n $NAMESPACE port-forward pod/default-100 8080:8080

Then, use the Vespa CLI to feed a document. In our same Album Recommendation sample:

vespa feed dataset/A-Head-Full-of-Dreams.json

Permissions

The Vespa Operator requires the following permissions. These permissions are listed by Kubernetes API verbs per resource.

Kubernetes Resource Required Permissions
CustomResourceDefinitions create, get, list, watch
VespaSet get, list, watch, create, update, patch, delete
VespaSet Subresources vespasets/status: update, patch
vespasets/finalizers: update
ConfigMaps get, list, watch, create, update, patch, delete
Services get, list, watch, create, update, patch, delete
Pods get, list, watch, create, update, patch, delete
Pod Execution get, create
Events create, patch
PersistentVolumeClaims get, list, watch, create, update, patch, delete
ServiceAccounts get, list, watch, create, update, patch, delete
Roles get, list, watch, create, update, patch, delete
RoleBindings get, list, watch, create, update, patch, delete

MiniKube Setup

MiniKube allows for simple local testing of Vespa on Kubernetes.

Initialize a Minikube cluster with 8 nodes, each with 4GiB of memory and 2 CPUs. Enable Minikube's image registry add-on to allow the Minikube nodes to access the image. In this example, we use podman as the driver, though docker is also valid.

# Start Minikube using an insecure registry. This is not recommended for production.
minikube start --nodes 8 --cpus 2 --memory 4GiB --driver=podman --insecure-registry="192.168.49.0/24"

# Enable the Image Registry add-on
minikube addons enable registry

# Verify MiniKube cluster was created
minikube status

Cache the images provided by our support team into the MiniKube registry.

# Authenticate to our registry
echo $VESPAAI_REGISTRY_TOKEN | podman login images.ves.pa \
  -u "$VESPAAI_REGISTRY_USER" \
  --password-stdin

# Cache the images locally
podman pull images.ves.pa/kubernetes/vespa:$VESPA_VERSION
podman pull images.ves.pa/kubernetes/operator:$VESPA_VERSION

Then, push the images to the MiniKube registry. It will be accessible from $(minikube ip):5000 on a standard setup.

# Save the minikube registry endpoint
export MINIKUBE_REGISTRY=$(minikube ip)

# Push the kubernetes/vespa image to the registry
podman tag  kubernetes/vespa:$VESPA_VERSION $MINIKUBE_REGISTRY:5000/localhost/kubernetes/vespa:$VESPA_VERSION
podman push --tls-verify=false $MINIKUBE_REGISTRY:5000/localhost/kubernetes/vespa:$VESPA_VERSION

# Push the kubernetes/operator image to the registry
podman tag  kubernetes/operator:$VESPA_VERSION $MINIKUBE_REGISTRY:5000/localhost/kubernetes/operator:$VESPA_VERSION
podman push --tls-verify=false $MINIKUBE_REGISTRY:5000/localhost/kubernetes/operator:$VESPA_VERSION

The images will now be available to the Minikube nodes at $MINIKUBE_REGISTRY:5000/localhost/kubernetes/operator:$VESPA_VERSION. We encourage saving the new image locations below.

export OCI_IMAGE_REFERENCE=$MINIKUBE_REGISTRY:5000/localhost/kubernetes/vespa
export OCI_IMAGE_REFERENCE_OPERATOR=$MINIKUBE_REGISTRY:5000/localhost/kubernetes/operator

Then, install the Local Persistent Volume Helm Chart. This will allow Persistent Volumes which are required by Vespa to be created in a MiniKube environment. Helm will automatically create a StorageClass called local-storage, which should be used as the StorageClass for subsequent steps.

# Clone the local persistent volume static provisioner from the Kubernetes sigs
$ git clone git@github.com:kubernetes-sigs/sig-storage-local-static-provisioner.git

# Install the Helm Chart onto the cluster globally
$ cd sig-storage-local-static-provisioner
$ helm install -f helm/examples/baremetal-default-storage.yaml local-volume-provisioner --namespace kube-system ./helm/provisioner

Create several usable volumes on each MiniKube Node. We recommend at least 4 per node for a smooth deployment.

# Create several volumes on each Minikube node.
$ for n in minikube minikube-m02 minikube-m03 minikube-m04 minikube-m05 minikube-m06 minikube-m07 minikube-m08; do
  echo "==> $n"
  minikube ssh -n "$n" -- '
    set -e
    for i in 1 2 3 4; do
      sudo mkdir -p /mnt/disks/vol$i
      if ! mountpoint -q /mnt/disks/vol$i; then
        sudo mount --bind /mnt/disks/vol$i /mnt/disks/vol$i
      fi
    done
    echo "Mounted:"
    mount | grep -E "/mnt/disks/vol[1-4]" || true
  '
done

Delete a VespaSet

Use the following helper script to delete all resources in a VespaSet. This will tear down all resources and should be used with caution.

  #!/bin/bash
# cleanup-k8s-resources.sh
# Usage:
#   ./cleanup-k8s-resources.sh  [--delete-operator] [--delete-namespace]
#   ./cleanup-k8s-resources.sh -n  [--delete-operator] [--delete-namespace]
#   ./cleanup-k8s-resources.sh --namespace  [--delete-operator] [--delete-namespace]
#
# Prompts for confirmation before proceeding. Use --dry-run to simulate.

set -euo pipefail

usage() {
  echo "Usage: $0  [--dry-run] [--delete-operator] [--delete-namespace]"
  echo "       $0 -n  [--dry-run] [--delete-operator] [--delete-namespace]"
  echo "       $0 --namespace  [--dry-run] [--delete-operator] [--delete-namespace]"
  echo
  echo "Options:"
  echo "  -n, --namespace     Kubernetes namespace to target"
  echo "      --dry-run       Show what would be deleted without making changes"
  echo "      --delete-operator  Also delete vespa-operator deployment and pods"
  echo "      --delete-namespace Delete the entire namespace (takes precedence)"
  echo "  -h, --help          Show this help"
}

NAMESPACE=""
DELETE_OPERATOR=0
DELETE_NAMESPACE=0

# Parse arguments
while [[ $# -gt 0 ]]; do
  case "$1" in
    -n|--namespace)
      [[ $# -ge 2 ]] || { echo "Error: $1 requires a value"; usage; exit 1; }
      NAMESPACE="$2"
      shift 2
      ;;
    --delete-operator)
      DELETE_OPERATOR=1
      shift
      ;;
    --delete-namespace)
      DELETE_NAMESPACE=1
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    -*)
      echo "Unknown option: $1"
      usage
      exit 1
      ;;
    *)
      if [[ -z "${NAMESPACE}" ]]; then
        NAMESPACE="$1"
        shift
      else
        echo "Unexpected argument: $1"
        usage
        exit 1
      fi
      ;;
  esac
done

if [[ -z "${NAMESPACE}" ]]; then
  echo "Error: namespace is required."
  usage
  exit 1
fi

echo "Target namespace: ${NAMESPACE}"
echo "This will operate on:"
if [[ $DELETE_NAMESPACE -eq 1 ]]; then
  echo "- Entire namespace (all resources will be deleted)"
else
  echo "- VespaSet resources"
  if [[ $DELETE_OPERATOR -eq 1 ]]; then
    echo "- Deployment vespa-operator"
    echo "- All Pods (including vespa-operator)"
  else
    echo "- Pods (excluding those with label app=vespa-operator)"
  fi
  echo "- Services (including cfg)"
  echo "- PersistentVolumeClaims"
fi

# Confirmation
if [[ $DELETE_NAMESPACE -eq 1 ]]; then
  read -r -p "The namespace '${NAMESPACE}' and all its resources will be deleted. Are you sure? [y/N]: " CONFIRM
elif [[ $DELETE_OPERATOR -eq 1 ]]; then
  read -r -p "Your resources (including vespa-operator deployment and pods) will be deleted. Are you sure? [y/N]: " CONFIRM
else
  read -r -p "Your resources will be deleted (operator pods are excluded). Are you sure? [y/N]: " CONFIRM
fi

case "$CONFIRM" in
  [yY]|[yY][eE][sS]) ;;
  *) echo "Aborted."; exit 0 ;;
esac

# Do not delete the operator, to ensure that the VespaSet finalizer completes
echo "Deleting Pods (excluding app=vespa-operator) in namespace: $NAMESPACE"
kubectl delete pods --grace-period=0  -l 'app!=vespa-operator' -n "$NAMESPACE" --ignore-not-found

echo "Deleting VespaSet resources in namespace: $NAMESPACE"
kubectl delete vespaset --all -n "$NAMESPACE" --ignore-not-found

echo "Deleting Services in namespace: $NAMESPACE"
kubectl delete svc -l 'app!=vespa-operator' -n "$NAMESPACE" --ignore-not-found
kubectl delete svc cfg -n "$NAMESPACE" --ignore-not-found

echo "Deleting PVCs in namespace: $NAMESPACE"
kubectl delete pvc  -l 'app!=vespa-operator' -n "$NAMESPACE" --ignore-not-found

# Delete Deployment if requested
if [[ $DELETE_OPERATOR -eq 1 ]]; then
  echo "Deleting Deployment 'vespa-operator' in namespace: $NAMESPACE"
  kubectl delete deployment vespa-operator -n "$NAMESPACE" --ignore-not-found
fi

if [[ $DELETE_NAMESPACE -eq 1 ]]; then
  echo "Deleting namespace: $NAMESPACE"
  kubectl delete namespace "$NAMESPACE" --ignore-not-found
  echo "✅ Cleanup complete: namespace deleted"
  exit 0
fi

echo "✅ Cleanup complete in namespace: $NAMESPACE"