Skip to content

Run Rocky on Kubernetes

The repository ships a Helm chart at deploy/helm/rocky/. It is community-supported: an example to start from, not a supported deployment. It encodes the deployment contract and refuses the settings that break it.

You create the Secret. The chart never creates one and accepts no secret value, so nothing secret reaches helm template output, the release state, or your shell history.

Terminal window
kubectl create namespace rocky
kubectl create secret generic rocky-serve -n rocky \
--from-literal=token="$(openssl rand -hex 32)"
helm install rocky ./deploy/helm/rocky -n rocky \
--set existingSecret.name=rocky-serve \
--set persistence.storageClassName=<a block-backed class>

Then put a project on the volume, and reach it:

8080/ui/
kubectl port-forward -n rocky svc/rocky 8080:8080

The chart does not deliver your project. The image mounts it at /data, so /data/rocky.toml and /data/models/ must exist. The state store is written to /data/models/.rocky-state.redb and the scheduler’s files to /data/.rocky/, so both live on the volume by construction.

Two ways. Pick one.

An init container that clones it. The right answer when the project lives in git, because a new pod always starts from the repository.

initContainers:
- name: clone
image: alpine/git:latest
args: ["clone", "--depth", "1", "https://github.com/you/your-project", "/data"]
volumeMounts:
- name: data
mountPath: /data

A pre-populated volume. Copy the project in once, before the server needs it.

Every refusal prints the reason. A schema can say a value is wrong; it cannot say why, so each rule lives in one place — the template that can explain itself.

Setting Refused Why
replicaCount anything but 1 One scheduler per project. A second pod inherits the watermarks and none of the scheduler’s memory, so both fire what is due
strategy anything but Recreate A rolling update overlaps two pods. That is two schedulers for the length of the rollout
terminationGracePeriodSeconds at or below drainTimeoutSeconds + 60 See below
persistence.accessMode anything but ReadWriteOnce A shared-access volume invites the second writer
persistence.storageClassName empty Never inherited from the cluster default. See below
scheduling.mode two schedulers at once It is one value: resident, cron or disabled
scheduling.cron.concurrencyPolicy anything but Forbid A tick that starts while the last one runs is a second scheduler
existingSecret.name empty The server binds 0.0.0.0 in a pod, and that requires a token
ingress.host empty when the Ingress is on The host is passed as --allowed-host; without it the page is reachable by a name the server refuses

On shutdown the server waits --drain-timeout-seconds (default 60) for a running scheduled child. A child still running then is not killed at once: it gets its own SIGTERM and a further fixed 60 seconds before SIGKILL.

drain the child --drain-timeout-seconds, default 60
then SIGTERM it a further 60s, fixed, not configurable
worst case 120s with the defaults

The chart defaults to 125 and refuses anything at or below drainTimeoutSeconds + 60, printing the arithmetic for your value.

The contract’s locking claims hold on block storage. An advisory flock on NFS or other network storage is unprobed, and ReadWriteOnce is an access mode, not proof of the filesystem underneath. The chart will not guess from the cluster default. Name a block-backed class, or point persistence.existingClaim at a claim you made deliberately.

scheduling.mode is a single value because two schedulers are two independent cursors over one set of pipelines.

resident the loop inside `rocky serve --scheduler`
cron a CronJob running `rocky tick` on the same volume
disabled neither

The advisory lock on .rocky/tick.lock does not make two of them safe. That lock is contention avoidance, not the correctness boundary: correctness lives in the claim state machine, and the contract’s rule stands regardless.

In cron mode the CronJob mounts the same claim as the Deployment. On a multi-node cluster a ReadWriteOnce claim pins both to one node.

Both probes use /api/v1/health, which is exempt from the bearer token and from the host check. It answers as soon as the listener is bound, which happens after the startup sweep attempt.

It does not prove the sweep succeeded, that the project compiled, that the state store is readable, or that the scheduler is making progress. A project that fails to compile still answers ok. The chart’s probe thresholds are conservative on purpose: a failed liveness probe restarts the pod, and a restart during a scheduled run is the loss window the contract describes.

serve.ui.enabled defaults to true. rocky serve --ui and --allowed-host shipped in engine-v1.74.0, the chart’s appVersion, so the default image serves the page at /ui/. Set it to false for the API alone. With scheduling.mode=resident the UI also needs existingSecret.webhookSecretKey: the page is handed a read-only token, and the scheduler’s webhook route must not be reachable with it.

With the UI on, the Ingress host is passed as --allowed-host and a request carrying any other Host is refused 421. Note that a foreign host usually never reaches Rocky at all: it matches no Ingress rule, so the controller’s own default backend answers 404 first.

helm upgrade replaces the pod in place on the same claim. The volume carries the run history, the watermarks and the scheduler’s cursors, so they survive the replacement. The PVC the chart creates is annotated helm.sh/resource-policy: keep, so uninstalling the release does not delete your project or its history.

Rolling back across a state-schema change follows the same rules as any other deployment. The deployment contract and the image guide cover what the engine does and does not promise.