Installation via Helm
Since v2.2.0, GPUStack can be deployed on Kubernetes with an in-cluster Higress API gateway using the official Helm chart.
Note
Deploying GPUStack on Kubernetes with Higress is currently considered experimental. Review the limitations before proceeding.
Prerequisites
| Component | Version |
|---|---|
| Helm | >= v3.18.4 |
| Kubernetes | >= v1.30.0 |
| GPUStack | >= v2.2.0 |
In addition:
- A default
StorageClassmust be configured in the cluster for the server's data volume (in k3s the default islocal-path). Alternatively, setserver.dataVolume.hostPathto use a host path volume instead of a PVC. - For GPU workers, ensure the appropriate GPU drivers and container toolkits are installed on the nodes (see Installation Requirements), and that Node Feature Discovery (NFD) is installed so GPU nodes are labeled.
Limitations
- The GPUStack server is deployed as a
StatefulSetand currently does not support more than one replica. - By default, the bundled embedded
PostgreSQLdatabase is used. It is recommended to specify an external database viaserver.externalDatabaseURLfor production. - Higress plugins are served by a dedicated
gpustack/higress-pluginsDeployment installed alongside GPUStack. The Higress gateway downloads plugins from this service on restart; if the service is unavailable, gateway startup is blocked until the plugins become accessible. - The bundled
higress-coresub-chart deploys Higress as the cluster's ingress controller. If another ingress controller is already running, sethigress-core.enabled=falseand pointgateway.ingressClassnameat an existing Higress instance.
Install k3s (optional)
The following steps use k3s as an example Kubernetes distribution. Other CNCF-conformant distributions (RKE2, kubeadm-based clusters, or managed cloud Kubernetes) work as well, as long as they meet the version requirements above.
Install k3s with Traefik disabled, since Higress is used as the ingress controller. For high-availability k3s clusters, refer to the k3s documentation.
Verify the setup:
Install GPUStack with Helm
The chart is published as an OCI artifact on Docker Hub at oci://registry-1.docker.io/gpustack/gpustack-chart. The chart version tracks the GPUStack release (e.g. chart 2.2.0 ships GPUStack v2.2.0).
Install the latest stable release. When --version is omitted, Helm resolves the newest stable chart version automatically and skips dev/pre-release builds:
To pin a specific version, append --version <chart-version> (the chart version matches the GPUStack release, e.g. 2.2.0):
By default, the higress-core sub-chart is enabled and deployed alongside GPUStack. If you already have Higress installed in your cluster, disable the bundled Higress and point GPUStack at your existing instance:
To customize parameters, use --set key=value or -f your-values.yaml during installation. See Chart Parameters below.
Installing from a Cloned Repository
Alternatively, clone the repository and install from the local chart directory (useful for air-gapped environments or when modifying the chart):
Installing Higress Separately
If you set higress-core.enabled=false, install a compatible Higress instance before deploying GPUStack:
Verify the IngressClass is available, and that its name matches gateway.ingressClassname:
For Higress customization, refer to the Higress documentation.
Accessing GPUStack
Wait for the server pod to become ready:
Retrieve the initial admin password:
If you did not set server.ingress.hostname, obtain the GPUStack UI address from the ingress:
Open the address in a browser and log in with username admin and the password retrieved above.
Common Configuration
Enabling Worker DaemonSets
Worker DaemonSets are disabled by default (worker.enabled=false). When enabled, the chart renders a CPU worker DaemonSet (<release>-worker), and one DaemonSet per GPU vendor listed in worker.gpuVendors (named <release>-worker-<vendor>).
Supported worker.gpuVendors values: nvidia, mthreads, amd, ascend, hygon, metax, iluvatar, cambricon, thead.
Note
Each GPU DaemonSet receives an automatic PCI-presence nodeSelector label (e.g. feature.node.kubernetes.io/pci-10de.present: "true" for NVIDIA), advertised by Node Feature Discovery. NFD must be installed in the cluster, otherwise no nodes carry the required labels and all worker pods stay Pending.
Whenever at least one GPU vendor is listed (i.e. worker.gpuVendors is non-empty), every worker pod additionally gets a required podAntiAffinity (topologyKey=hostname) so two workers cannot share a node — this protects the hostNetwork: true ports from collision.
The CPU worker DaemonSet covers the nodes no GPU vendor claims, through the nodeSelector feature.gpustack.ai/acceleratable: "false". Set worker.cpuEnabled=false to leave those nodes alone — typically when the control plane shares the cluster with the GPU nodes and must not gain workers:
worker.gpuVendors must then name at least one supported vendor; otherwise the release would render no worker DaemonSet at all and the install is refused. The GPU DaemonSets keep their -<vendor> suffix either way, so turning the CPU one off never renames one of them to <release>-worker.
Alternatively, add GPU clusters and worker nodes through the UI on the Clusters and Workers pages after installation.
Using an External Database
By default GPUStack uses the embedded PostgreSQL database. To use an external PostgreSQL or MySQL database, set server.externalDatabaseURL:
Enabling HTTPS with a Custom Certificate
Provide the certificate and key contents (PEM) via server.ingress.tls. When both are set, the ingress schema becomes HTTPS:
Pulling Images From a Private Registry
To pull all images (GPUStack server/worker, higress-plugins, and the bundled higress-core gateway/controller/pilot) from a mirrored private registry, override global.hub. This relies on Helm's global-values propagation, so a single setting covers every image:
global.hub is also passed to the server as GPUSTACK_SYSTEM_DEFAULT_CONTAINER_REGISTRY, ensuring inference engine images (e.g. vLLM, llama.cpp) are pulled from the same registry.
To supply pull credentials, the chart can create a docker-registry Secret named gpustack-image-pull-secret and wire it into all pods:
To reference your own pre-existing Secrets instead, replace global.imagePullSecrets:
Chart Parameters
The most commonly used parameters are listed below. For the complete and authoritative list, see the chart's values.yaml and README.
| Parameter | Default | Description |
|---|---|---|
debug | false | Enable debug mode. |
registrationToken | null | Worker registration token; a random one is generated and reused if null. |
clusterDomain | cluster.local | Kubernetes cluster service domain suffix. |
global.hub | docker.io | Container registry host; override for a private registry. |
global.imagePullSecrets | [gpustack-image-pull-secret] | Image pull Secrets attached to all pods and propagated to sub-charts. |
global.nodeSelector | {} | Default nodeSelector for every component; replaced by component-level value. |
image.repository | gpustack/gpustack | Image repo with namespace; final ref is {global.hub}/{repository}:{tag}. |
image.tag | null | Image tag; defaults to the chart's appVersion. |
image.pullPolicy | IfNotPresent | Image pull policy. |
imagePullSecret.credentials.registry | docker.io | Registry host used when the chart creates the pull Secret. |
imagePullSecret.credentials.username | null | Registry username; creates the Secret when set with password. |
imagePullSecret.credentials.password | null | Registry password; creates the Secret when set with username. |
server.ingress.hostname | null | Ingress hostname for the server. |
server.ingress.tls.cert | null | Ingress TLS certificate (PEM); enables HTTPS when set with key. |
server.ingress.tls.key | null | Ingress TLS private key (PEM). |
server.externalDatabaseURL | null | External database connection string (PostgreSQL or MySQL). |
server.dataVolume.hostPath | null | Host path for the server data volume; uses hostPath instead of a PVC. |
server.dataVolume.size | 100Gi | Server data volume size (PVC). |
server.apiPort | 30080 | API service port. |
server.metricsPort | 10161 | Server metrics port. |
server.environmentConfig | {} | Extra environment variables for the GPUStack server. |
server.nodeSelector | {} | Server pod nodeSelector; replaces global.nodeSelector when non-empty. |
gateway.ingressClassname | higress | Higress IngressClass name; enables in-cluster gateway mode when found. |
higress-core.enabled | true | Deploy the bundled Higress gateway; disable if already installed. |
worker.enabled | false | Render worker DaemonSets. |
worker.gpuVendors | [nvidia] | GPU vendors; one DaemonSet per vendor plus the CPU DaemonSet. |
worker.cpuEnabled | true | Render the CPU worker DaemonSet; false requires a GPU vendor. |
worker.nodeSelector | {} | Base worker nodeSelector; replaces global.nodeSelector when non-empty. |
worker.port | 10150 | Worker service port. |
worker.metricsPort | 10151 | Worker metrics port. |
worker.dataDir | /var/lib/gpustack | Host path mounted at /var/lib/gpustack inside each worker pod. |
worker.environmentConfig | {} | Extra environment variables for the GPUStack worker. |
Uninstallation
The release deploys more than the server: the gpustack-operator sub-chart brings the Kueue and Node Feature Discovery controllers, and the operator creates custom resources that carry finalizers. helm uninstall removes the controllers with everything else, so the custom resources have to be gone first — deleted while the controllers are still running — or the objects stay in Terminating forever and the gpustack-system namespace never leaves it. In order:
-
Drain the workloads while every controller is still running. The operator cleans up the finalizers on its
Instanceobjects, and Kueue refuses to release aClusterQueuewhile admittedWorkloads still occupy it — so these have to go before anything is switched off.Instances are namespaced and live in the tenants' namespaces, so enumerate all of them with-A. Delete the models from the server (or their instances directly); each instance's pod terminates with it, and the Workload goes with the Instance. Do not delete the namespace's pods wholesale — that takes the operator, Kueue and NFD controllers down with it, and they are what finish the cleanup:If Kueue is shared with other applications, do not use
--allhere or in step 3 — delete only GPUStack's objects. Everything the operator creates in Kueue is named with agpustackprefix (clusterqueue/gpustack--…,admissioncheck/gpustack-node-devices). The cluster-scoped kinds select by name alone; the namespaced ones need the namespace carried along, because-o nameomits it: -
Stop the operator's own controllers, but leave the release installed so the Kueue controllers keep running — they are what finalize the deletions in the next step. As long as
gpustack-operator-workerand thegpustack-operator-device-manager-*DaemonSets run, they recreate theLocalQueues,ClusterQueues and thegpustack-node-devicesAdmissionCheck as soon as you delete them, so the counts never reach zero: -
Delete the Kueue resources in dependency order —
LocalQueues, thenClusterQueues, thenAdmissionChecks, thenResourceFlavors. Kueue holds thekueue.x-k8s.io/resource-in-usefinalizer on an object whose users are still present: thegpustack-node-devicesAdmissionCheck stays in deletion until every ClusterQueue is gone, and each ClusterQueue until its LocalQueues and admitted Workloads are. Wait for each kind to be empty before moving to the next. On a shared cluster, apply thegpustack-prefix scoping from step 1 to every command instead of--all: -
Uninstall the release. Do not pass
--wait: the Node Feature Discovery post-delete prune hook blocks the deletion wait for longer than its--timeout, and killing Helm to escape it leaves the prune RBAC behind ownerless (step 5 removes those by hand — and run step 5 even without--wait): -
Remove what
helm uninstalldoes not. Helm never deletes CRDs, and the operator registers admission webhooks and APIServices that no release owns — both fail closed against the now-gone operator Service, and the webhooks reject the finalizer strip below until they are deleted:The
worker.gpustack.aigroup is always GPUStack's own — no other component uses it — so its CRDs are safe to delete unconditionally. -
Delete the CRDs the release leaves behind — only the ones this release installed. Deleting a CRD deletes every object of that kind in the whole cluster, so if the cluster already ran Kueue, Node Feature Discovery or Higress before GPUStack (see
higress-core.enabledabove) and anything else still uses them, leave their CRDs in place and skip this step for those groups. Discover what this release owns rather than guessing:On a stock install that lists Kueue's CRDs, and NFD's and Higress's if the chart installed them:
The cluster provider's own CRDs are never GPUStack's to delete —
*.k3s.cattle.ioandhelm.cattle.ioon a k3s cluster, for instance, belong to the cluster. -
(Optional) Delete the namespace, which takes the Higress objects, the bootstrap ConfigMap, ServiceAccount and the registration token Secret with it. Know what goes with it: the namespace holds the server's PVC (
gpustack-data-dir-gpustack-server-0,<volumeClaimTemplate>-<statefulset>-<ordinal>), which carries the server's persisted data — the embedded PostgreSQL database with its users, models and clusters. On aDeletereclaim policy (thelocal-pathdefault) the underlying volume is deleted as well. Keep the namespace if you want the data for a reinstall; otherwise delete the namespace (and the PVC first, if the reclaim policy does not already remove it):