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.
Prerequisites
Section titled “Prerequisites”- A Kubernetes cluster and
kubectlpointed 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), andcosignto verify a signed release.
Install
Section titled “Install”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).
git clone https://github.com/bantex01/VectorStep-Dist.gitcd VectorStep-Dist/k8skubectl create namespace vectorstep1. 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.
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:
kubectl -n vectorstep apply -f gateway/pvc.yaml -f gateway/configmap.example.yaml \ -f gateway/deployment.yaml -f gateway/service.yamlkubectl -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-gatewaykubectl -n vectorstep rollout status deployment/vectorstepDeploy the Gateway first — the service’s config points at the Gateway’s Service
(vectorstep-gateway:18780).
3. Check it’s up.
kubectl -n vectorstep port-forward svc/vectorstep 8000:8000 &curl -s http://127.0.0.1:8000/healthThen open http://127.0.0.1:8000/ui/ and log in with the admin token:
kubectl -n vectorstep get secret vectorstep-secrets \ -o jsonpath='{.data.VECTORSTEP_ADMIN_TOKEN}' | base64 -dThe deploy tutorial goes on to create a pipeline and trigger it end to end.
Image tags and versions
Section titled “Image tags and versions”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.12image: ghcr.io/bantex01/vectorstep-gateway:0.1.12To 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: 1withstrategy: 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) whendatabase.auto_migrateistrue(the default). Withreplicas: 1andRecreatethat’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, setauto_migrate: falseand runkubectl exec … alembic upgrade head(or a one-shotJob) 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.
Changing config after install
Section titled “Changing config after install”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.
Security hardening
Section titled “Security hardening”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.yamlruns 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.yamlredirectsHOMEand thenpm/uvxcache 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.
TLS and the Ingress
Section titled “TLS and the Ingress”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.
Troubleshooting
Section titled “Troubleshooting”| 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 |
Where next
Section titled “Where next”- Deploy tutorial — the install walked through step by step, with auth, a NetworkPolicy and TLS.
- Changing config on Kubernetes — shipping pipeline and agent changes from CI.
- Following the tutorials on Kubernetes — using the rest of the docs against a cluster.
- Adding MCP tool servers — giving agents tools, including pre-installing servers in your own image.
- Scaling & availability and Deployment — what to plan for in production.