Skip to main content
Platforms

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 kubectl access
  • 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.

Next steps