Skip to content

Architecture

NebariApp (optional)

Kubernetes Services

RayService CRD

RayCluster

ray:// :10001

HTTP :8000

HTTPS

KubeRay Operator

Manages RayService lifecycle

Head Pod

:8265 dashboard

:8000 serve

:10001 client

Worker Pod(s)

Ray Workers

-head-svc

:8265 :10001 :6379

-serve-svc

:8000

HTTPRoute + OIDC auth

via Envoy Gateway

Jupyter Notebook

(in-cluster)

Browser

(external)

KubeRay operator watches RayService resources. It creates the underlying RayCluster, deploys the Serve applications named in serveConfigV2, monitors their health, and performs zero-downtime upgrades when the config changes.

RayService is the chart’s central object. It carries two things: the Serve config, and the cluster config. The chart writes both from values — serve.proxyLocation and serveApplications into serveConfigV2, and the head/worker specs into rayClusterConfig.

The head pod runs the GCS (Ray’s metadata store), the dashboard, the Serve controller, and — with proxyLocation: EveryNode — an HTTP proxy. It exposes four ports: 6379 GCS, 8265 dashboard, 10001 Ray client, 8000 Serve HTTP.

Worker pods run Ray workers, and Serve replicas land on them. One group, groupName: workers, sized by worker.replicas between minReplicas and maxReplicas.

RayService creates its own stable Services — but only after every Serve application reports healthy. With the default empty serveApplications that condition never holds, so the dashboard and Serve endpoint would be unreachable on a fresh install.

The chart therefore renders both itself, selecting the head pod directly:

selector:
ray.io/node-type: head
app.kubernetes.io/name: kuberay

They exist from the moment the chart installs, regardless of Serve state. Both carry argocd.argoproj.io/compare-options: IgnoreExtraneous, which tells Argo CD to skip them during comparison when they are live in the cluster but absent from the desired state — so KubeRay adopting and rewriting them does not surface as drift or trigger a prune.

Note that serve-svc targets only the head pod, even under proxyLocation: EveryNode. The per-node proxies serve direct-to-pod traffic; the Service does not load-balance across them. This is also why worker readiness has no effect on user-visible HTTP routing — the reason the chart can safely simplify the worker probes.

serveConfigV2: |
proxy_location: {{ serve.proxyLocation }}
http_options:
host: "0.0.0.0"
port: 8000
applications: [...]

host: "0.0.0.0" is set here so the proxy binds all interfaces from the start — without it Serve binds loopback and nothing outside the pod can reach it. That is why there is no manual serve start step anywhere in this pack.

In-clusterExternal
Clientnotebooks, other podsbrowsers, API clients
RouteClusterIP ServiceNebariApp → HTTPRoute → Envoy
AuthnoneOIDC at the gateway, when enabled
Protocolsray:// and HTTPHTTPS

The split is not an oversight. The Ray client protocol cannot traverse an OIDC redirect, so notebooks must reach the head service directly. Access control on that path is NetworkPolicy, not identity — anything permitted to reach :10001 can submit arbitrary code to the Ray cluster, which is worth scoping deliberately.

Serve and dashboard get separate resources with separate hostnames, because they are separate audiences with different exposure appetites. The recommended posture keeps the serve endpoint internal (serve.enabled: false) and exposes only the dashboard.

Both inherit the same auth and gateway settings — there is no per-endpoint override.

Two features render nothing at all when unused, so the output is byte-identical to a plain install:

  • CA bundle — an initContainer, volumes, mounts, and four environment variables on both pod specs, only when orgCABundle.configMapName is set.
  • GPU toleration — an nvidia.com/gpu toleration, only when that group’s resources mention the GPU resource, and only when you have not defined one yourself.

Both live under spec.rayClusterConfig, which is what makes the Argo CD ignoreDifferences rule on that path consequential rather than cosmetic. See Deploying on Nebari.

StateWhereSurvives a cluster roll
Declarative Serve applicationsserveConfigV2 in the RayServiceyes
Applications deployed via serve.run()the running Ray clusterno
Model codethe container imageyes
Anything written to a pod filesystemthe podno

The chart provisions no persistent volumes. Anything that must survive belongs in the image or in external storage.