Skip to content

Runners on Kubernetes

The kubernetes executor creates a pod per job: an init container fetches the workspace, the job’s container.image (or a default) runs the steps, service containers run as sidecars, and the pod is deleted afterwards. Nothing is shared between jobs.

The Slipway chart can deploy runners next to (or without) the control plane. They are off by default because runners usually live next to the workloads they build, often in another cluster.

values.yaml
runner:
enabled: true
executor: kubernetes # kubernetes | docker (docker-in-docker, privileged)
workload: Deployment # or DaemonSet
replicaCount: 2
serverUrl: https://slipway.example.com
labels: [linux, k8s]
capacity: 4 # concurrent jobs per runner process
token:
existingSecret: slipway-runner-token # key: token
kubernetes:
jobNamespace: slipway-jobs
rbac: true # Role + RoleBinding for pods, secrets, PVCs in jobNamespace
jobServiceAccount: default
defaultJobImage: docker.io/library/ubuntu:24.04
terminationGracePeriodSeconds: 3600 # let running jobs finish when draining
Terminal window
kubectl create namespace slipway-jobs
kubectl -n slipway create secret generic slipway-runner-token --from-literal=token=<registration token>
helm upgrade --install slipway-runners ./deploy/helm/slipway -n slipway -f values.yaml

A minimal runner Deployment, ServiceAccount and Role:

apiVersion: v1
kind: ServiceAccount
metadata: { name: slipway-runner, namespace: slipway }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: slipway-runner, namespace: slipway-jobs }
rules:
- apiGroups: [""]
resources: [pods, pods/log, pods/exec, secrets, persistentvolumeclaims]
verbs: [create, get, list, watch, delete]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: slipway-runner, namespace: slipway-jobs }
subjects: [{ kind: ServiceAccount, name: slipway-runner, namespace: slipway }]
roleRef: { apiGroup: rbac.authorization.k8s.io, kind: Role, name: slipway-runner }
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: slipway-runner, namespace: slipway }
spec:
replicas: 2
selector: { matchLabels: { app: slipway-runner } }
template:
metadata: { labels: { app: slipway-runner } }
spec:
serviceAccountName: slipway-runner
terminationGracePeriodSeconds: 3600
securityContext: { runAsNonRoot: true, runAsUser: 10001, seccompProfile: { type: RuntimeDefault } }
containers:
- name: runner
image: ghcr.io/slipway-systems/slipway-runner:latest
args: [runner]
env:
- { name: SLIPWAY_RUNNER_SERVER, value: https://slipway.example.com }
- { name: SLIPWAY_RUNNER_EXECUTOR, value: kubernetes }
- { name: SLIPWAY_RUNNER_K8S_NAMESPACE, value: slipway-jobs }
- { name: SLIPWAY_RUNNER_LABELS, value: "linux,k8s" }
- { name: SLIPWAY_RUNNER_CONCURRENCY, value: "4" }
- name: SLIPWAY_RUNNER_TOKEN
valueFrom: { secretKeyRef: { name: slipway-runner-token, key: token } }
securityContext: { allowPrivilegeEscalation: false, readOnlyRootFilesystem: false, capabilities: { drop: [ALL] } }
resources: { requests: { cpu: 100m, memory: 128Mi }, limits: { memory: 1Gi } }

Verify the exact verbs the runner needs for your version with slipway runner check against the cluster; the Helm chart’s Role is the reference.

# runner config file
kubernetes:
namespace: slipway-jobs
service_account: default # account the job pod runs as
default_image: ubuntu:24.04
init_image: alpine/git:latest
workspace_size: 10Gi
image_pull_secrets: [registry-creds]
node_selector: { workload: ci }
cpu_request: 500m
cpu_limit: "4"
memory_request: 1Gi
memory_limit: 8Gi
pod_start_timeout: 5m

executor: docker in the chart runs the runner with a privileged rootful dind sidecar. Use it only on isolated, tainted nodes. Prefer the Kubernetes executor with a rootless builder (BuildKit, Kaniko or Buildah) for image builds.

Runners only need outbound access to the Slipway server (and to registries and SCMs your jobs use). The chart can generate NetworkPolicies (networkPolicy.enabled, networkPolicy.egress.restrict). Job pods in jobNamespace should be denied access to the cluster API and cloud metadata endpoints unless a job needs them.