Skip to main content
A Kubernetes cluster is one compute in Expanse. One Helm chart installs a daemon that watches pods and jobs (and the batch systems built on them: Volcano, Kueue, Argo Workflows, Flux, Ray, Flyte) and reports each run to the console: lifecycle, requested against used resources, GPU activity and, when enabled, where in the code a run spends its time. Workloads are untouched. The console’s Add compute panel shows the short-lived install token and your control-plane and data-plane URLs. The token is prompted for during install and never goes on a command line or into Helm values.

Prefer to delegate? Copy this into an AI assistant with kubectl and Helm access; it fetches this guide itself, so it always follows the current steps.

Prerequisites

  • Console access in your Expanse organisation (any member can register).
  • A workstation with kubectl access to the cluster and Helm 3.
  • Outbound HTTPS from the daemon pod to your Expanse control and data planes. Both planes must be up before the pod starts: token exchange needs the data plane, discovery and the first heartbeat need both, and the compute stays pending until that heartbeat.
  • For Python stack attribution or the local run hop, a daemon namespace that admits privileged pods: the node sampler then runs with hostPID and as root, and the install notes print the label to set. Pods opt in to stack attribution with the expanse.sh/code-attribution: enabled annotation or a namespace in nodeSampler.codeAttribution.namespaces; the sampler reads at most nodeSampler.codeAttribution.readsPerSecond stacks a second per node, 10 by default, shared fairly across the node’s opted-in pods.
GPU nodes, especially HGX/NVSwitch systems: disable unattended package upgrades and apply updates during planned maintenance with the node drained. Never restart nvidia-fabricmanager while GPUs are in use. Reboot after kernel or NVIDIA driver updates before admitting GPU workloads again.A fabric-manager restart under live GPUs can leave NVLink fabric stale: NCCL collectives may fail even while single-GPU metrics look healthy. If this happens, drain and reboot the affected node. Kubernetes preflight cannot check the host’s unattended-upgrade configuration; the node administrator must verify it.

1. Register the compute

In the console, open Compute → Add compute, pick Kubernetes, and generate an install token.

2. Exchange the token

On your workstation, with the URLs from the Add compute panel:
This writes the compute’s long-lived credential to ~/.expanse/config.json and a first pull credential for the Expanse image registry to ~/.expanse/registry-dockerconfig.json; neither is printed. If the install fails before the config file is written, rerun it with the same token; retry stops working at the compute’s first heartbeat or when the token expires. --namespace selects the namespace used by every command the CLI prints. Use the same --kubeconfig and --context flags on preflight and install when selecting a file or context explicitly. Install carries these selectors into its printed kubectl and Helm commands. Without --namespace, both use the selected kubeconfig context’s namespace, or default when the context has none. If no usable context exists, install requires --namespace before exchanging the token. An explicit namespace works without kubeconfig.

3. Create the namespace and the Secrets

The chart uses existing Secrets and never creates one. Keep the Secrets and the release in the same namespace; this guide uses expanse-system.
The Expanse image registry is private. Create its pull Secret from the file the install step wrote:
The dedicated field manager prevents kubectl from copying credentials into last-applied-configuration, including on Secrets created by older CLI versions. The chained annotation removal clears previously copied credentials and is safe to rerun when the annotation is absent. If the install step printed that it could not issue a pull credential, generate one in the console under Platform → Registry and select Copy token. Run the fallback block printed by the CLI in Bash with base64 and jq installed, then paste the registry host and token at its prompts. That block creates the Secret in the selected namespace and context using server-side apply, then removes its temporary credential file.

4. Install the chart

Set the Secret name, a config revision and the registry pull refresher. The chart version you install names the daemon release, so the values carry no image tag. Change the revision whenever you replace the config Secret so every daemon pod restarts with that config.
For certificate injection, DNS registration or admission policies, add annotations and labels to the values for each affected component before installing. With registryPull.enabled, the chart runs a small refresher that writes a fresh token into expanse-registry every 4 hours (registryPull.refreshInterval) and adds the Secret to every chart pod’s imagePullSecrets, so nobody handles a pull token after install. The refresher may read and update that one Secret and nothing else; it never creates it, so step 3 comes first. Keep the setting in values.yaml rather than on the command line so every upgrade keeps it. Without the refresher, the token in the Secret expires 12 hours after it was issued: list expanse-registry under imagePullSecrets and rerun the console’s command before a helm upgrade, a node replacement or any other fresh pull. The same 12 hours apply to a console credential used because the install step could not issue one. Clusters that pull only from their own mirror set image.repository; a mirror needs no expiring Secret.
Everything else has a default. helm show values expanse/expanse-daemon prints every value with its comment, and the values reference at the end of this page lists them all. In production set state.persistence.enabled=true so queued source uploads and the daemon’s cached endpoints and identity survive pod replacement.

5. Verify

The compute becomes active after its first authenticated heartbeat, normally within a minute of the pod starting. If nothing arrives, read kubectl -n expanse-system logs deploy/expanse-daemon.

Upgrade

Upgrade the release in place to the chart version you are moving to:
Each chart version pins one daemon release, its appVersion, and that release’s image digest, so all pods pull one pinned version and the chart version is the only thing an upgrade changes. A values file that still sets image.tag stays on that release until you remove it. With registryPull.enabled the pull Secret is already fresh; without it, re-issue the Secret from the Registry page first if the cluster pulls from the Expanse registry, since its credential lasts 12 hours. The observer restarts once. With state.persistence.enabled=false its queued source uploads sit on an emptyDir and do not survive that restart, so enable persistence before the first production upgrade. Turning nodeSampler.cupti, nodeSampler.dcgm or nodeSampler.codeAttribution on or off is a rolling restart of the node sampler DaemonSet, one GPU node at a time; the workloads on those nodes are not touched. The liveness probe (probes.enabled) is on by default and needs daemon v0.5.0 or newer, which carries the healthcheck subcommand it runs. On an older image every probe fails and the kubelet restarts the pods, so set probes.enabled=false there.

Replace a lost compute credential

If the config Secret is deleted, or the cluster is rebuilt, the compute keeps its identity. On the compute’s console page choose Re-issue install token, then exchange it from a workstation with the CLI (step 2 installs it):
  • Cluster intact, Secret lost: recreate the Secret and restart the daemon workloads so they load the new credential. The daemon reads its config once at startup; until then the old pods loop on rejected heartbeats. If the Secret still exists, --force-conflicts replaces its configuration even when another field manager owns it.
    Add kubectl -n expanse-system rollout restart daemonset/expanse-daemon-node-sampler when nodeSampler.enabled=true, and kubectl -n expanse-system rollout restart deployment/expanse-daemon-registry-pull when registryPull.enabled=true.
  • Cluster rebuilt: nothing is there to restart; repeat steps 3 and 4 with the exchanged config.
The previous credential stops working at the exchange. The token can be exchanged once.

Optional: GPU metrics

The default install observes workload lifecycle only. On GPU clusters, enable the node sampler DaemonSet: it ships node and per-GPU metrics and attributes each GPU to the pod holding it, non-root by default.
The sampler runs on every node by default. CPU-only nodes still get host and per-pod usage and Python stacks; they report no GPU chips, so the GPU health summary counts GPU nodes only. Set nodeSelector to save the pods on large CPU pools or when a RuntimeClass is required. The sampler reads the host /proc and /sys, so its namespace must allow privileged pods:
GPU-to-pod attribution runs non-root: the sampler joins the processes nvidia-smi lists on each card to their pod through the read-only host /proc. nodeSampler.podResources.source=podresources reads the kubelet pod-resources socket instead, which also names the owner of a card whose pod has not started a GPU process yet, at the cost of running the sampler as root. nodeSampler.podResources.enabled=false drops per-pod attribution and the per-pod CPU and memory series altogether.

Optional: GPU profiling (CUPTI and DCGM)

Both live in the node sampler and are off by default. Nothing is installed on the node, and Expanse never runs a DCGM host engine of its own.
Without a reachable engine the DCGM chip on the compute page reads disabled with the reason and the standard GPU metrics keep flowing. To keep counters restricted, retain RmProfilingAdminOnly=1 on the node. The default nodeSampler.cupti.mode=perfmon needs Linux 5.8+ and grants CAP_PERFMON without forcing root for counters. The pod-resources attribution source and Python attribution require root independently. sysadmin explicitly grants CAP_SYS_ADMIN and forces root for older hosts; none grants neither. Replace the retired cupti.sysAdmin key with cupti.mode when upgrading.

Optional: Python stack attribution

nodeSampler.codeAttribution.enabled=true lets the node sampler read the Python stacks of pods that opted in: a pod annotated expanse.sh/code-attribution: enabled, or one in a namespace listed in nodeSampler.codeAttribution.namespaces; nothing else is sampled. The sampler reads at most nodeSampler.codeAttribution.readsPerSecond stacks a second per node, 10 by default, shared fairly across the node’s opted-in pods. An execution page then shows where in the code a run spends its time. Python only; compiled workloads get no stacks. The sampler then needs the host PID namespace, CAP_SYS_PTRACE, root and read-only pods access, which the chart applies. System namespaces and the release’s own pods are never sampled, and watch.namespaces scopes it like the observer. GPU profiling and stack attribution need daemon v0.5.0 or newer: a chart whose appVersion is v0.5.0 or later, with no older image.tag set.

Optional: in-process GPU profiling agent

Deep captures widen the out-of-process counters by default. To also record kernel activity, NVTX ranges and, where the driver allows, PC samples inside the workload, enable agent injection and label the GPU pods that may receive it:
The observer’s admission webhook adds the agent library, a control mount and four environment variables to a labelled GPU pod in a labelled namespace when it is created; the node sampler stages the library and arms captures for those pods only. A mutated pod gains a hostPath volume, which the Pod Security baseline and restricted profiles refuse, so an opted-in namespace must run at the privileged level and the webhook never touches a namespace labelled as enforcing either. Without cert-manager, set agentInjection.tls.certManager.enabled=false and point agentInjection.tls.existingSecret and agentInjection.tls.caBundle at your own serving pair. Pods created before the webhook existed keep running unprofiled until restarted.

Optional: namespace-scoped install

Set watch.namespaces and rbac.clusterWide=false: the chart grants namespace permissions only where it watches, plus the refresher’s get and update on the pull Secret, and renders nothing cluster-scoped. The queue view then reports degraded, because queue and capacity snapshots need one cluster-scoped read, list nodes; grant it with rbac.listNodes=true when an administrator allows. Per-pod GPU attribution and stack attribution also read pods cluster-wide; leave both off for a strictly namespace-scoped install.

Optional: network policy

The daemon dials only your control and data planes, the API server and, when configured, the DCGM host engine. Turn on the chart’s NetworkPolicy and narrow the plane addresses:
On EKS with Pod Identity add networkPolicy.extraEgress for 169.254.170.23/32 on TCP 80.

Optional: pin the image digest

A chart from charts.expanse.sh already pins its daemon release by digest. If you set image.tag yourself, set image.digest to the sha256:... from that release’s manifest beside it; a re-pushed tag can then never change what the cluster runs.

Optional: OpenShift

Set podSecurityContext: null so the SCC’s uid range applies to the observer. The second line is needed only with the node sampler.