Skip to content
Beta. VectorStep is in beta.Let us know if something breaks.

Kubernetes

VectorStep runs on Kubernetes as two Deployments — the service (webhooks, pipelines, UI) and the Gateway (the agent runtime) — each with a ClusterIP Service, a PersistentVolumeClaim, and a ConfigMap, pulling the published multi-architecture (amd64 and arm64) images from GHCR. The manifests are plain, commented YAML you copy and adapt; there is no Helm chart. Everything is visible and editable, and it drops straight into Kustomize, Argo CD or Flux if that’s how you manage the rest of your cluster.

If you’d rather follow one start-to-finish walkthrough, the deploy tutorial takes you through the whole install command by command, including a NetworkPolicy, authentication and TLS. This page is the reference: what’s in the manifests, what they assume, and what to know before you apply them.

  • A Kubernetes cluster and kubectl pointed at it. A throwaway local cluster (kind, minikube) works for trying it out.
  • Storage. Both Deployments use a ReadWriteOnce PersistentVolumeClaim (5 Gi for the service, 1 Gi for the Gateway), so the cluster needs a default StorageClass or pre-provisioned volumes.
  • An LLM provider key (Anthropic, OpenRouter, Google, Azure OpenAI, or a reachable Ollama) for the Gateway. Any placeholder is enough to bring the services up; agents can’t call a model without a real one.
  • openssl (or any way to generate random tokens).
  • Optional, depending on how far you take it: an ingress controller for browser access, a CNI that enforces NetworkPolicy (Calico and Cilium do; kind’s and minikube’s defaults don’t), and cosign to verify a signed release.

The manifests live in the public VectorStep-Dist repo under k8s/: k8s/gateway/ and k8s/service/, each with deployment.yaml, service.yaml, configmap.example.yaml and pvc.yaml, plus k8s/service/ingress.example.yaml, k8s/networkpolicy.example.yaml, and k8s/delivery/ (for shipping config changes from CI).

Terminal window
git clone https://github.com/bantex01/VectorStep-Dist.git
cd VectorStep-Dist/k8s
kubectl create namespace vectorstep

1. Generate the Gateway’s tokens first. On Kubernetes the Gateway’s two tokens are an input, not something you extract after it boots — generate both before applying anything, and the Gateway uses them as given instead of minting its own (the opposite of a docker compose install). admin is for whoever authors agents (a Gateway MCP client, or curl against the write endpoints); invoke is what the service uses to run agents. See Gateway authentication for exactly what each grants.

Terminal window
GATEWAY_ADMIN_TOKEN="$(openssl rand -hex 24)"
GATEWAY_INVOKE_TOKEN="$(openssl rand -hex 24)"
kubectl -n vectorstep create secret generic vectorstep-gateway-secrets \
--from-literal=VECTORSTEP_GATEWAY_ADMIN_TOKEN="$GATEWAY_ADMIN_TOKEN" \
--from-literal=VECTORSTEP_GATEWAY_INVOKE_TOKEN="$GATEWAY_INVOKE_TOKEN" \
--from-literal=ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY"
kubectl -n vectorstep create secret generic vectorstep-secrets \
--from-literal=VECTORSTEP_GATEWAY_TOKEN="$GATEWAY_INVOKE_TOKEN" \
--from-literal=VECTORSTEP_ADMIN_TOKEN="$(openssl rand -hex 24)" \
--from-literal=VECTORSTEP_WEBHOOK_TOKEN="$(openssl rand -hex 24)"

(Using a provider other than Anthropic? Put its key in the same Secret and reference it from the Gateway’s ConfigMap — see Gateway providers.)

Put the invoke token — never the admin one — in the service’s VECTORSTEP_GATEWAY_TOKEN: the service only reads and runs agents, it never writes them. Swapping them isn’t a validation error; it’s a 403 the first time the service calls the Gateway. The service also refuses to start without an admin token (VECTORSTEP_ADMIN_TOKEN is the one you log into the UI with).

2. Apply the Gateway, then the service. Copy and edit each configmap.example.yaml if you need to (at minimum the Gateway’s LLM provider), then:

Terminal window
kubectl -n vectorstep apply -f gateway/pvc.yaml -f gateway/configmap.example.yaml \
-f gateway/deployment.yaml -f gateway/service.yaml
kubectl -n vectorstep apply -f service/pvc.yaml -f service/configmap.example.yaml \
-f service/deployment.yaml -f service/service.yaml
kubectl -n vectorstep rollout status deployment/vectorstep-gateway
kubectl -n vectorstep rollout status deployment/vectorstep

Deploy the Gateway first — the service’s config points at the Gateway’s Service (vectorstep-gateway:18780).

3. Check it’s up.

Terminal window
kubectl -n vectorstep port-forward svc/vectorstep 8000:8000 &
curl -s http://127.0.0.1:8000/health

Then open http://127.0.0.1:8000/ui/ and log in with the admin token:

Terminal window
kubectl -n vectorstep get secret vectorstep-secrets \
-o jsonpath='{.data.VECTORSTEP_ADMIN_TOKEN}' | base64 -d

The deploy tutorial goes on to create a pipeline and trigger it end to end.

The manifests reference :latest. That’s fine for evaluation, but pin both images to the same released tag for anything you depend on: with imagePullPolicy: Always a pod restart can otherwise pick up a newer release.

image: ghcr.io/bantex01/vectorstep:0.1.12
image: ghcr.io/bantex01/vectorstep-gateway:0.1.12

To check what a running pod actually is, kubectl exec deploy/vectorstep -- python -c "import os;print(os.environ['VECTORSTEP_VERSION'], os.environ['VECTORSTEP_COMMIT'])", the version at the bottom of the UI sidebar, or /health — a latest tag in the manifest can’t tell you, and with IfNotPresent different nodes may hold different builds of it.

Image tags have no leading v (the Git tags do — v0.1.12). The service and the Gateway are released and supported as a matched pair: there’s no wire-version negotiation between them, so always run the same version of both. Every tagged image is signed and has an SBOM attached to its GitHub release — see Verifying a release to check what you’re pulling before it reaches your cluster.

Constraints worth knowing before you apply

Section titled “Constraints worth knowing before you apply”
  • The service runs replicas: 1 with strategy: Recreate, and that’s required regardless of database backend. The scheduler is in-process and the dedup/event state is in-memory, so a second replica would double-fire scheduled pipelines. PostgreSQL doesn’t change this; it only removes SQLite’s single-writer limit. See Scaling & availability for what that does and doesn’t mean, and what’s planned.
  • Migrations run in-process at boot (create_tables(), see Deployment → Database) when database.auto_migrate is true (the default). With replicas: 1 and Recreate that’s safe and needs no init container — the old pod is fully gone before the new one starts. If your DBAs want migrations as a reviewed, separate step, set auto_migrate: false and run kubectl exec … alembic upgrade head (or a one-shot Job) before rolling the new image.
  • Secrets are delivered as Kubernetes Secrets referenced via envFrom, feeding the same ${VAR} placeholders the config uses everywhere else — never baked into a ConfigMap or an image.

For the quick version — where each file lives and how to reload — see Changing configuration → Kubernetes.

Pipelines, steps and agents hot-reload — no pod restart. Three routes get changes from CI into a running cluster (API push, kubectl copy + reload, and a GitOps ConfigMap route with a reload sidecar), each with copy-and-tweak templates in k8s/delivery/. See Changing config on Kubernetes for how to choose between them. And if you’re working through the quick start or tutorials, which are written for the Docker Compose install, Following the tutorials on Kubernetes maps their commands and paths onto a cluster.

Both deployments pass Pod Security Admission at restricted unmodified — runAsNonRoot, a dropped capability set, no privilege escalation, seccompProfile: RuntimeDefault, and readOnlyRootFilesystem: true. Two things follow from readOnlyRootFilesystem, and both are already handled in the shipped manifests:

  • The service’s deployment.yaml runs a small init container that creates /data’s expected subdirectories before the main container starts. A freshly provisioned PVC is genuinely empty — unlike a Docker named volume, which gets the image’s existing content copied in the first time it’s used — so without this the pod crash-loops on first boot.
  • The Gateway’s deployment.yaml redirects HOME and the npm/uvx cache directories onto its PVC. Both spawn MCP servers that download packages at runtime and write under $HOME, which is read-only otherwise; redirecting the caches onto the PVC also means they persist across pod restarts instead of re-downloading every time.

k8s/networkpolicy.example.yaml — copy, edit, apply; not applied by default — adds two ingress-only policies: the Gateway accepts its port only from pods labelled app: vectorstep, and the service accepts its port only from your ingress controller’s namespace and whatever namespace your webhook senders live in. Whether either does anything depends on your cluster’s CNI actually enforcing NetworkPolicy. See the Threat model and Securing a deployment for the broader network-placement picture this fits into.

service.yaml’s Service is ClusterIP — reachable in-cluster only. k8s/service/ingress.example.yaml is the recommended way to reach it from outside: an Ingress with a tls: block, and a commented cert-manager.io annotation for clusters that mint certificates automatically instead of using a manually provisioned Secret. This is where most deployments terminate browser-facing TLS — at the ingress controller, not in the pod.

The in-cluster hop (service → Gateway) is a different story. Both configmap.example.yaml files carry a commented server.tls block (same shape as Deployment → TLS), with the mount path matching a commented tls secret volume in the matching deployment.yaml — enable it for a verified wss:// hop rather than relying on the namespace’s NetworkPolicy alone. The shipped default is plain ws://vectorstep-gateway:18780/rpc: in-cluster traffic inside a NetworkPolicy-protected namespace is a defensible place to leave it, and the manifests don’t force TLS setup just to get you running. k8s/service/configmap.example.yaml has the wss:// alternative commented alongside it.

Reaching the Gateway from outside the cluster

Section titled “Reaching the Gateway from outside the cluster”

If something outside the cluster needs the Gateway — a Gateway MCP client, say — keep its Service ClusterIP and use kubectl port-forward for anything short-lived, or an Ingress of its own with authentication in front of it: the Gateway’s admin token can rewrite agent definitions. See k8s/README.md.

Symptom Cause
Service pod CrashLoopBackOff, exit code 132, no logs A Linux VM on an Apple M4 advertising CPU features it doesn’t have — the fix
Service pod crashes with auth.tokens entry 'admin' has an empty token VECTORSTEP_ADMIN_TOKEN is missing from vectorstep-secrets
403 the first time the service calls the Gateway The admin and invoke tokens are swapped — the service must hold the invoke token
Calls return 401 Every route needs a token — see Securing a deployment