Kubernetes
Run Rivet workers on Kubernetes.
Workers run in runner mode here: the pod opens a connection out to the control plane on startup. It needs no Service, no Ingress, and no public URL, only outbound network access.
Requirements
- A Kubernetes cluster with
kubectlaccess - Container registry credentials
- A control plane, either Rivet Cloud or your own
Steps
Package your app
FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]
Rivet requires Node 22 or newer. Alpine images need a build toolchain because Rivet’s SQLite dependency has no musl prebuilt binary, so the Debian-based image is the smaller working choice. A runner-mode worker listens on no port.
Build and push
docker build -t registry.example.com/your-team/rivet-worker:latest .
docker push registry.example.com/your-team/rivet-worker:latest
Configure the connection
apiVersion: v1
kind: Secret
metadata:
name: rivet-worker-secrets
type: Opaque
stringData:
RIVET_ENDPOINT: <your-rivet-endpoint>
RIVET_PUBLIC_ENDPOINT: <your-rivet-public-endpoint>
Both values are described in Workers. RIVET_ENDPOINT is the only one a runner-mode worker needs to connect. Set RIVET_PUBLIC_ENDPOINT as well when browser clients read connection details from this worker.
The name is scoped to the worker so it does not collide with the control plane’s rivet-engine-secrets if you run both in one namespace.
Create the runner config
A worker is refused until a runner config exists for its pool. Against a self-hosted control plane, create it once:
curl -X PUT "http://<control-plane-host>:6420/runner-configs/default?namespace=default" \
-H "Authorization: Bearer $RIVET_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"datacenters":{"default":{"normal":{}}}}'
Rivet Cloud does this for you. See Runner configs.
Deploy
terminationGracePeriodSeconds matters here. Actors drain for up to 30 minutes, and 2100 covers that plus shutdown overhead. The Kubernetes default of 30 seconds cuts actors off mid-drain.
apiVersion: apps/v1
kind: Deployment
metadata:
name: rivet-worker
spec:
replicas: 1
selector:
matchLabels:
app: rivet-worker
template:
metadata:
labels:
app: rivet-worker
spec:
terminationGracePeriodSeconds: 2100
containers:
- name: rivet-worker
image: registry.example.com/your-team/rivet-worker:latest
envFrom:
- secretRef:
name: rivet-worker-secrets
kubectl apply -f rivet-worker-secrets.yaml
kubectl apply -f deployment.yaml
Verify
kubectl get pods -l app=rivet-worker
NAME READY STATUS RESTARTS AGE
rivet-worker-7d4c8b9f5d-x2klm 1/1 Running 0 20s
Once the pod is ready it appears under Runners in the dashboard. Nothing needs registering.
Autoscaling
Runner-mode workers are interchangeable, so replicas can be raised freely or driven by a HorizontalPodAutoscaler on CPU. Nothing needs re-registering; new pods dial out and appear under Runners.
kubectl autoscale deployment rivet-worker --cpu-percent=60 --min=2 --max=10
Scale-down is the part that needs care. A pod the autoscaler removes still drains for up to 30 minutes under terminationGracePeriodSeconds: 2100, so it keeps serving its actors long after the HPA has stopped counting it. Set a scale-down stabilization window that matches, or the autoscaler will keep removing pods that have not actually released their capacity.
If you expose the worker
A worker does not need an Ingress to run actors. If you add one anyway, raise its idle timeout to 3600 seconds: workers connect over WebSocket and the defaults will sever them. See Ports.