Running FLARE in Kubernetes
NVIDIA FLARE can be deployed to Kubernetes by first provisioning normal startup
kits and then preparing each server or client kit for the Kubernetes runtime.
The prepared kit contains a participant-specific Helm chart plus the
startup/ and local/ folders that must be staged into Kubernetes storage.
For example scripts that automate temporary Kubernetes and managed cloud cluster testing flows, see examples/devops. These scripts are for development, smoke testing, demos, and learning only; they are not production deployment guidance.
Prerequisites
Before you start, make sure you have:
nvflareinstalled on the workstation where you provision and runnvflare deploy prepare.kubectlconfigured for the target cluster. Use akubectlversion that is compatible with the Kubernetes API server.Helm 3.
A Kubernetes cluster with standard
apps/v1Deployment,rbac.authorization.k8s.io/v1Role/RoleBinding, Service, Secret, and PVC support.An actively supported Kubernetes release. The generated chart uses stable Kubernetes APIs and does not depend on provider-specific extensions.
A default
StorageClassor an explicitstorageClassNamefor every PVC. Check withkubectl get storageclass.A container registry that every server and client cluster can pull from.
NVIDIA GPU Operator or NVIDIA device plugin installed on clusters that will run jobs with
resource_spec[site].num_of_gpus. See Cloud GPU Setup References.For Kubernetes job launching, a Kubernetes API-server CA chain that passes Python 3.13+ strict X.509 validation. CA certificates must include required RFC 5280 extensions such as
keyUsagewith certificate signing allowed.
The generated charts do not install a Kubernetes cluster, storage class, GPU device plugin, ingress controller, or registry credentials.
Cloud GPU Setup References
Managed Kubernetes services differ in how they handle GPU drivers, the NVIDIA
Container Toolkit, the NVIDIA GPU Operator, and the NVIDIA Kubernetes device
plugin. Before running GPU jobs, verify that GPU nodes advertise allocatable
nvidia.com/gpu resources.
Use the current provider documentation for your cluster:
Amazon Elastic Kubernetes Service (EKS): Manage NVIDIA GPU devices on Amazon EKS and GPU support in eksctl.
Google Kubernetes Engine (GKE): Manage the GPU Stack with the NVIDIA GPU Operator on GKE and About GPUs in GKE.
Azure Kubernetes Service (AKS): Use GPUs on AKS and NVIDIA GPU Operator with AKS.
NVIDIA: NVIDIA GPU Operator.
Kubernetes Runtime Model
Kubernetes deployment has two runtime layers:
A parent pod runs the long-lived FLARE server or client process. Helm installs this pod from the per-participant
helm_chart/generated bynvflare deploy prepare. The parent pod mounts the configured workspace PVC atparent.workspace_mount_pathand readsstartup/andlocal/from that PVC. Its Python executable is set byparent.python_pathor, when omitted, defaults to/usr/local/bin/python3.A job pod is created dynamically by
ServerK8sJobLauncherorClientK8sJobLauncherfor each submitted job. Job pod image, Python path, CPU, memory, GPU, and ephemeral storage settings come from the submitted job’slauncher_specand from thejob_launcherdefaults ink8s.yaml.
The generated Helm chart does not run submitted jobs directly. It installs the parent participant process, its Kubernetes Service, its ServiceAccount, and the Role/RoleBinding that allow the launcher to create job pods.
The parent Service is the stable in-cluster address for dynamically launched job
pods. nvflare deploy prepare patches the prepared kit’s internal
communication settings to use the generated Service name and parent_port.
parent_port is the parent-process port used by job pods for internal
parent/job communication; it is not the federated learning port that remote
clients use to reach the server. If you rename or replace the Service, keep the
Service name, Service port, and prepared kit communication settings consistent.
The runtime shape is:
admin console
|
| FL/admin traffic to server fed_learn_port/admin_port
v
server cluster or namespace
server parent pod
| mounts workspace PVC: startup/, local/, transfer/
| launches server job pods through Kubernetes API
v
server job pod emptyDir workspace
| optional mounts: /data/<study>/<dataset> from study-data PVCs
| workspace transfer over parent Service on parent_port
client cluster or namespace
client parent pod
| outbound FL connection to server fed_learn_port
| mounts workspace PVC: startup/, local/
| launches client job pods through Kubernetes API
v
client job pod emptyDir workspace
| optional mounts: /data/<study>/<dataset> from study-data PVCs
| workspace transfer over client parent Service on parent_port
Server and client participants may run in the same Kubernetes cluster or in
separate clusters. Separate clusters are common because each site controls its
own compute and data. If participants run in separate clusters, using the same
namespace and PVC names in each cluster is safe. If multiple participants run in
one cluster, give each participant its own namespace or its own workspace PVC;
do not point a server and a client at the same workspace PVC because their
startup/ and local/ contents are different.
Client sites need outbound network access to the server endpoint configured
during provisioning, usually <server-host>:<fed_learn_port>. A client site
does not need an inbound FL port or an externally exposed Service. The client
chart creates an in-cluster Service only so that dynamically launched client job
pods can reach their client parent pod.
Each prepared participant folder contains its own chart:
server-k8s/
helm_chart/
local/
startup/
transfer/
site-1-k8s/
helm_chart/
local/
startup/
transfer/
The transfer/ directory is the normal FLARE admin file-transfer directory.
For the server, it is used under the mounted workspace when admin storage is
configured as transfer. It is not the Kubernetes job workspace-transfer
mechanism and job pods do not mount it. Stage or create it on the server
workspace PVC when you stage startup/ and local/.
Build and Push the FLARE Image
The Helm charts need a FLARE runtime image that every participating cluster can pull. For the image build and registry-push workflow, see Build and Push the FLARE Image.
NVIDIA publishes an official NVFlare Docker image in the NGC container registry
at nvcr.io. Use a tag that matches the NVFlare version used to provision and
prepare the startup kits, and set that image in parent.docker_image in
k8s.yaml.
Users can also build their own parent runtime image from this repository by
modifying docker/Dockerfile.parent and pushing the result to a registry that
all participating clusters can pull from. Keep the NVFlare K8S extra, or
install the Kubernetes Python client explicitly, so the parent server or client
can create job pods.
The parent image comes from parent.docker_image in k8s.yaml and is
rendered into helm_chart/values.yaml. Submitted jobs must also specify a job
image in meta.json under launcher_spec[site][k8s].image or
launcher_spec.default.k8s.image. The parent image and job image can be the
same image, but they do not have to be.
Prepare Startup Kits
The provisioning step remains responsible for identity material, certificates, server host names, FL ports, and FLARE configuration:
nvflare provision -p project.yml -w workspace
The server default_host and host_names in project.yml must match the
external endpoint that clients and admin consoles will use to reach the server.
If those values change, reprovision and rerun nvflare deploy prepare.
After provisioning, prepare each server or client startup kit with
nvflare deploy prepare:
nvflare deploy prepare workspace/<project>/prod_00/server \
--output server-k8s \
--config k8s.yaml
nvflare deploy prepare workspace/<project>/prod_00/site-1 \
--output site-1-k8s \
--config k8s.yaml
Example k8s.yaml:
runtime: k8s
namespace: nvflare
parent:
docker_image: registry.example.com/nvflare:dev
image_pull_secrets:
- registry-credentials
parent_port: 8102
workspace_pvc: nvflws
workspace_mount_path: /var/tmp/nvflare/workspace
python_path: /usr/local/bin/python3
resources:
requests:
cpu: "2"
memory: 8Gi
job_launcher:
config_file_path:
default_python_path: /usr/local/bin/python3
image_pull_secrets:
- job-registry-credentials
pending_timeout: 300
The runtime config controls site-level Kubernetes settings:
namespaceis where the parent pod and dynamically launched job pods run.server_service_namesets the FL server Kubernetes Service name. It defaults tonvflare-server.parentvalues are rendered into the Helm chart. They set the parent image, Python executable, workspace PVC, parent service port, parent pod resources, optional parent pod security context, and optional image pull Secret references.parent.image_pull_secretsmust name Kubernetes Secrets that already exist in the target namespace; NVFLARE does not create registry credentials. This setting applies to the generated parent pod chart; usejob_launcher.image_pull_secretsfor dynamically launched job pods.parent.python_pathcontrols the long-lived SP/CP parent pod command.parent.workspace_mount_pathis also written into the K8s launcher config so spawned SJ/CJ job pods mount their job workspace and startup kit at the same in-container path.job_launchervalues are written into the participant’slocal/resources.json.defaultso the parent process can create job pods.config_file_pathmay be empty for in-cluster configuration, anddefault_python_pathcontrols SJ/CJ job pods when a job does not overridelauncher_spec[site][k8s].python_path. It does not control the SP/CP parent pod Python path; useparent.python_pathfor that command.image_pull_secretsnames existing Kubernetes image pull Secrets attached to every dynamically launched job pod for this prepared site. Configure this during deployment preparation when job images live in a private registry; job authors still only specify the job image inmeta.json.pending_timeoutis in seconds. It controls how long a dynamically launched job pod can stay inPendingorUnknownbefore the launcher deletes it and reports the run as an execution exception. The adminlist_jobscommand then showsFINISHED:EXECUTION_EXCEPTIONinstead of treating the timeout as a user abort.
The parent pod and job pods use different Python settings:
Setting |
Applies to |
Notes |
|---|---|---|
|
Parent server or client pod |
Rendered as the Helm container command for |
|
Dynamically launched job pods |
Used when a job does not set
|
|
Dynamically launched job pods |
Per-job override in |
Prepare Cluster Storage
Create and bind any workspace or study-data PVCs required by your cluster before starting the participant.
Create the namespace before applying namespaced PVC manifests or installing the Helm chart:
export NAMESPACE=nvflare
kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
Workspace PVC
The workspace PVC is for the parent server or client pod. The generated chart
mounts parent.workspace_pvc at parent.workspace_mount_path, but it does
not upload files to the PVC. Copy the prepared kit’s startup/ and
local/ directories into the root of that workspace PVC before installing the
chart. For server kits, also create or copy transfer/ at the workspace root
for admin file-transfer storage.
Example workspace-pvc.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nvflws
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
# If your cluster has no default StorageClass, uncomment and set this.
# storageClassName: <storage-class-name>
Use a larger size if the server’s job history, snapshots, or logs need more space. Use a distinct workspace claim per participant when multiple participants run in the same namespace.
For example, with a prepared folder named server-k8s and a workspace PVC
named nvflws:
export NAMESPACE=nvflare
export PREPARED_KIT=server-k8s
export WORKSPACE_PVC=nvflws
kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
kubectl -n "$NAMESPACE" apply -f workspace-pvc.yaml
kubectl -n "$NAMESPACE" get pvc "$WORKSPACE_PVC"
kubectl -n "$NAMESPACE" delete pod nvflare-pvc-copy --ignore-not-found=true
cat >/tmp/nvflare-pvc-copy.json <<EOF
{
"spec": {
"restartPolicy": "Never",
"volumes": [
{"name": "ws", "persistentVolumeClaim": {"claimName": "${WORKSPACE_PVC}"}}
],
"containers": [
{
"name": "nvflare-pvc-copy",
"image": "busybox:1.36",
"command": ["sleep", "600"],
"volumeMounts": [{"name": "ws", "mountPath": "/mnt/nvflws"}]
}
]
}
}
EOF
kubectl -n "$NAMESPACE" run nvflare-pvc-copy \
--image=busybox:1.36 \
--restart=Never \
--overrides="$(cat /tmp/nvflare-pvc-copy.json)"
kubectl -n "$NAMESPACE" wait --for=condition=Ready pod/nvflare-pvc-copy --timeout=120s
kubectl -n "$NAMESPACE" exec nvflare-pvc-copy -- rm -rf /mnt/nvflws/startup /mnt/nvflws/local
kubectl -n "$NAMESPACE" cp "$PREPARED_KIT/startup" nvflare-pvc-copy:/mnt/nvflws/startup
kubectl -n "$NAMESPACE" cp "$PREPARED_KIT/local" nvflare-pvc-copy:/mnt/nvflws/local
kubectl -n "$NAMESPACE" exec nvflare-pvc-copy -- mkdir -p /mnt/nvflws/transfer
kubectl -n "$NAMESPACE" exec nvflare-pvc-copy -- ls -la /mnt/nvflws
kubectl -n "$NAMESPACE" delete pod nvflare-pvc-copy
The PVC root must contain startup/ and local/ directly. At runtime,
those folders appear under the configured workspace mount path
(parent.workspace_mount_path, rendered as
persistence.workspace.mountPath). With the example default, the parent
expects /var/tmp/nvflare/workspace/startup and
/var/tmp/nvflare/workspace/local. If the PVC root contains a nested
server-k8s/ or site-1-k8s/ folder instead, the parent pod will not find
those folders under the configured mount path.
The dynamically launched job pod does not mount this workspace PVC. Each job
pod receives its own writable emptyDir mounted at the configured workspace
mount path. The launcher transfers the needed local/ and job workspace
content into that emptyDir when the pod starts and uploads the job results
back to the parent process when the job exits. The job pod workspace size is
controlled by
launcher_spec[site][k8s].ephemeral_storage when set, or by the launcher
default otherwise. The same value is also used for the container
ephemeral-storage request and limit.
Study Data PVC
Study data PVCs are separate from the parent workspace PVC. Configure optional
study data mappings in local/study_data.yaml inside the prepared kit before
copying local/ into the workspace PVC. If the kit is already staged, edit
the file on the PVC or restage local/.
Example study_data.yaml:
default:
data:
source: nvfldata
mode: ro
For Kubernetes, each source value is a PVC claim name. The job pod mounts
the dataset at /data/<study>/<dataset>, for example
/data/default/data. mode must be ro or rw. Missing
study_data.yaml files or missing entries for a job’s study mean no
study-data PVCs are mounted for that job.
Example nvfldata-pvc.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nvfldata
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
# If your cluster has no default StorageClass, uncomment and set this.
# storageClassName: <storage-class-name>
Use an access mode supported by your storage backend. ReadWriteOnce is
enough for many single-node or single-job cases. Use ReadOnlyMany or
ReadWriteMany storage, or separate per-site claims, when multiple job pods
on different nodes need concurrent access to the same dataset.
Apply the study-data PVC in the same namespace where the participant’s job pods will run:
kubectl -n "$NAMESPACE" apply -f nvfldata-pvc.yaml
kubectl -n "$NAMESPACE" get pvc nvfldata
Install the Charts
Prepare, stage, and install each server or client kit in the Kubernetes cluster or namespace where that participant runs.
Install the server chart:
export NAMESPACE=nvflare
helm upgrade --install server server-k8s/helm_chart \
--namespace "$NAMESPACE"
Install a client chart with the same pattern:
helm upgrade --install site-1 site-1-k8s/helm_chart \
--namespace "$NAMESPACE"
nvflare deploy prepare writes image.repository and image.tag into
helm_chart/values.yaml from parent.docker_image in k8s.yaml. For a
different parent image, rerun nvflare deploy prepare with the updated
k8s.yaml. If you must override the image at Helm install or upgrade time,
prefer a values file and pass it to every related helm upgrade command:
cat > server-values.yaml <<'EOF'
image:
repository: registry.example.com/nvflare
tag: dev
EOF
helm upgrade --install server server-k8s/helm_chart \
--namespace "$NAMESPACE" \
-f server-values.yaml
Avoid using one-off --set image.repository=... and --set image.tag=...
flags as the source of truth for image changes. Later upgrade commands that do
not include the same overrides can render the release with the generated chart
defaults instead.
If the server and client run in the same namespace, use different workspace PVCs
or override persistence.workspace.claimName for one of the releases:
helm upgrade --install site-1 site-1-k8s/helm_chart \
--namespace "$NAMESPACE" \
--set persistence.workspace.claimName=nvflws-site-1
The namespace must already exist before you run namespaced kubectl commands
or install the charts. The storage step above creates it explicitly. If you skip
that flow, create the namespace first:
kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
Expose FL Traffic
The generated server chart creates a Kubernetes Service for the FL server. The
service defaults to ClusterIP, which is reachable only inside the cluster.
If clients or admin consoles connect from outside the cluster, expose the FL
server ports with the mechanism that matches your Kubernetes environment:
If you use an override values file for the server release, include the same
-f file in these helm upgrade commands too.
Use a cloud load balancer when available:
helm upgrade --install server server-k8s/helm_chart \ --namespace "$NAMESPACE" \ --set service.type=LoadBalancer kubectl -n "$NAMESPACE" get svc nvflare-server
For local testing from the same machine, use port forwarding:
kubectl -n "$NAMESPACE" port-forward svc/nvflare-server 8002:8002 8003:8003
For single-node or ingress-based clusters, configure your cluster’s TCP routing, firewall rules, or host ports so the FL and admin ports from
project.ymlreach thenvflare-serverService. Some single-node deployments use--set hostPortEnabled=truefor the server chart.
Make sure the server host name used during provisioning resolves to the exposed
address. For example, update DNS or /etc/hosts for the admin console and for
any remote client sites.
Verify The Deployment
After installing a chart, verify that the deployment, pods, services, and PVCs are healthy:
kubectl -n "$NAMESPACE" rollout status deployment/server --timeout=300s
kubectl -n "$NAMESPACE" rollout status deployment/site-1 --timeout=300s
kubectl -n "$NAMESPACE" get pods,svc,pvc
kubectl -n "$NAMESPACE" logs deploy/server --tail=200
kubectl -n "$NAMESPACE" logs deploy/site-1 --tail=200
If a pod is not ready, inspect the pod and recent events:
kubectl -n "$NAMESPACE" describe pod -l app.kubernetes.io/instance=server
kubectl -n "$NAMESPACE" get events --sort-by=.lastTimestamp
Pod logs persist only while the pod exists. When a parent pod restarts or is recreated by a Helm upgrade, prior logs are lost. Use cluster log aggregation or capture logs externally if you need to retain them.
Login With The Admin Console
Use the admin startup kit produced by nvflare provision. The admin console
connects to the server host and ports written into the provisioned project, so
confirm that those names resolve to the exposed Kubernetes endpoint before
logging in.
cd workspace/<project>/prod_00/admin@nvidia.com/startup
bash fl_admin.sh
When prompted for User Name, enter the admin identity from project.yml,
such as admin@nvidia.com.
Private Registry and Image Pull Secrets
The generated chart supports parent-pod image pull Secrets through
imagePullSecrets in helm_chart/values.yaml. nvflare deploy prepare
fills this value from parent.image_pull_secrets in k8s.yaml. The
Kubernetes Secrets must already exist in the participant namespace; NVFLARE does
not create registry credentials.
For example:
kubectl -n "$NAMESPACE" create secret docker-registry registry-credentials \
--docker-server=registry.example.com \
--docker-username="$REGISTRY_USERNAME" \
--docker-password="$REGISTRY_PASSWORD"
parent:
docker_image: registry.example.com/nvflare:dev
image_pull_secrets:
- registry-credentials
This renders the parent chart value as:
imagePullSecrets:
- name: registry-credentials
Dynamically launched job pods are not controlled by the Helm chart after
installation. For private job images, set job_launcher.image_pull_secrets in
k8s.yaml before running nvflare deploy prepare. The K8s launcher writes
those Secret references into each created job pod’s spec.imagePullSecrets.
If your cluster supports node-level registry credentials or the namespace
default ServiceAccount already has suitable image pull Secrets, you can use that
instead of explicit image_pull_secrets settings.
If a parent pod or job pod enters ImagePullBackOff, inspect the pod events
with kubectl describe pod and confirm that the image name, tag, registry
credentials, and image pull policy are correct.
Helm Values Reference
nvflare deploy prepare writes each participant’s generated defaults to
helm_chart/values.yaml. The most commonly overridden values are image,
service exposure, resources, and persistence.
Value |
Scope |
Default source |
Purpose |
|---|---|---|---|
|
Server and client |
Participant name |
Deployment name and chart labels unless chart helpers derive another name. |
|
Client |
Participant name |
Client UID passed to |
|
Server and client |
|
Kubernetes Service name used by job pods to reach the parent pod. |
|
Server and client |
Repository part of |
Parent pod image repository. |
|
Server and client |
Tag part of |
Parent pod image tag. If empty, the repository value is used as-is. |
|
Server and client |
|
Parent pod image pull policy. |
|
Server and client |
|
Parent pod image pull Secret references. The Secrets must already exist in the release namespace. |
|
Server and client |
|
Creates a ServiceAccount for the parent pod. |
|
Server and client |
|
Adds annotations to the generated ServiceAccount. |
|
Server and client |
|
Must remain enabled when the parent launcher uses in-cluster Kubernetes API access. |
|
Server and client |
|
Creates the Role and RoleBinding needed to create job pods and startup Secrets. |
|
Server and client |
|
Adds annotations to the parent pod template. |
|
Server and client |
|
Parent pod security context. |
|
Server and client |
|
Parent pod resource requests and limits. |
|
Server and client |
|
Workspace PVC mounted by the parent pod. |
|
Server and client |
|
Internal volume name in the parent pod manifest. |
|
Server and client |
|
In-container workspace mount path. |
|
Server |
Server |
FL server port exposed by the server Service and parent container. |
|
Server |
Server |
Admin port exposed by the server Service and parent container. |
|
Server |
|
Internal parent Service port for server job pods. |
|
Client |
|
Internal parent Service port for client job pods. |
|
Server |
|
Adds |
|
Server |
|
Emits a MicroK8s nginx ingress TCP-services ConfigMap mapping the FL ports to the server Service. Useful only on MicroK8s clusters that use the nginx ingress addon. |
|
Server |
|
Server Service type, for example |
|
Server |
|
Optional static load-balancer IP when supported by the cluster. |
|
Server and client |
|
Adds annotations to the generated Service. |
|
Server and client |
|
Parent container command. |
|
Server and client |
Generated by |
Parent process module and runtime arguments. Override only when you know how the FLARE parent process is launched. |
Launcher RBAC
The generated chart creates a ServiceAccount and namespace-scoped Role/RoleBinding by default. The launcher needs permission to:
create, delete, get, list, and watch pods;
create, get, and update Secrets.
The Secret permission is required because the launcher creates or updates a
per-site startup-kit Secret for dynamically launched job pods. Job pods mount
that Secret read-only at <workspace_mount_path>/startup. The Secret name
uses this pattern:
nvflare-startup-<rfc1123-site-name>-<8-char-sha256-prefix>
<rfc1123-site-name> is the site name with non-RFC1123 characters replaced.
The 8-char SHA256 suffix is always appended, even for site names that are
already RFC1123-compliant, so look up the Secret name with the grep example
below rather than constructing it. Service and Deployment names, in contrast,
track the site name directly when it is already DNS-label compliant (lowercase
alphanumeric and hyphens, starting and ending with alphanumeric, up to 63
characters).
For example, inspect startup-kit Secrets with:
kubectl -n "$NAMESPACE" get secret | grep nvflare-startup
If your cluster operator disables serviceAccount.create or rbac.create
in chart values, provide equivalent API access in the same namespace before job
submission. The parent pod must run with a ServiceAccount that can create job
pods and create or update startup-kit Secrets.
Configure Kubernetes Job Pods
Job pod settings live in the submitted job’s meta.json under
launcher_spec. The default block applies to all sites and a site-specific
block overrides it:
{
"launcher_spec": {
"default": {
"k8s": {
"image": "registry.example.com/nvflare-job:latest",
"python_path": "/usr/local/bin/python3",
"cpu": "2",
"memory": "8Gi",
"ephemeral_storage": "8Gi"
}
},
"site-1": {
"k8s": {
"image": "registry.example.com/site-1-job:latest",
"cpu_request": "1",
"memory_request": "4Gi"
}
}
},
"resource_spec": {
"site-1": {
"num_of_gpus": 1
}
}
}
Supported launcher_spec[site][k8s] keys include:
image: container image for the job pod. This is required, either inlauncher_spec.default.k8sor in the site-specifick8sblock.python_path: Python executable inside the job image. If omitted, the launcher usesjob_launcher.default_python_pathfrom the prepared site runtime config.cpuandmemory: container limits. Whencpu_requestormemory_requestis omitted, the request matches the corresponding limit.cpu_requestandmemory_request: optional requests when the request should be lower than the limit.ephemeral_storage: Kubernetes quantity string for the job workspaceemptyDir.sizeLimitand the containerephemeral-storagerequest and limit. Set this inlauncher_spec.default.k8sor in a site-specificlauncher_spec[site].k8sblock. If omitted, the built-in launcher default is used. The currentdeploy prepareruntime config does not exposejob_launcher.ephemeral_storageas ak8s.yamlsetting.
Job pods are created with imagePullPolicy: Always. Tag changes take effect
immediately, but every submitted job pulls the image once per site. For private
registries, factor this into rate limits and registry-credential plumbing. Use
job_launcher.image_pull_secrets when dynamically launched job pods need
explicit image pull Secrets.
resource_spec remains scheduler-facing. New jobs should place K8s launcher
settings in launcher_spec and resource requests such as num_of_gpus in
resource_spec. The launcher writes resource_spec[site].num_of_gpus as
both the nvidia.com/gpu request and limit.
GPU requests require the NVIDIA GPU Operator or NVIDIA device plugin on the
target cluster. For MIG, make sure the device plugin exposes a resource that the
launcher requests. The built-in launcher writes nvidia.com/gpu for
num_of_gpus; clusters that expose only profile-specific resources such as
nvidia.com/mig-1g.5gb require cluster configuration or launcher
customization to request those resource names.
Reprovisioning and Upgrades
Provisioned certificates, local config, server communication settings, and
prepared Kubernetes parent-Service settings are tied to the provisioned project
state. If you change project.yml, server host names, ports, participants,
or k8s.yaml settings:
Run
nvflare provisionagain.Run
nvflare deploy prepareagain for every affected participant.Back up any PVC content you need to keep before restaging. On the server workspace PVC, that usually includes
transfer/(admin uploads), the site directory holding job history and snapshots, and any log files at the workspace root. Client workspace PVCs typically have little to preserve beyond optional logs.Replace the staged
startup/andlocal/folders on the participant’s workspace PVC. Remove stale copies first so old certificates or config files do not remain.Run
helm upgradefor the affected release.
Do not reuse an old staged startup/ or local/ folder after
reprovisioning.
Troubleshooting
PVC stays Pending
Check that the cluster has a default storage class, or add an explicit
storageClassName under each PVC spec:
kubectl get storageclass
kubectl -n "$NAMESPACE" describe pvc nvflws
kubectl -n "$NAMESPACE" describe pvc nvfldata
Use storageClassName: "" only when binding to a pre-created PersistentVolume
without a dynamic storage class.
Parent pod has ImagePullBackOff
Confirm that the parent image exists and that the cluster can pull it:
kubectl -n "$NAMESPACE" describe pod -l app.kubernetes.io/instance=server
kubectl -n "$NAMESPACE" describe pod -l app.kubernetes.io/instance=site-1
Check the rendered image:
helm -n "$NAMESPACE" get values server --all
helm -n "$NAMESPACE" get values site-1 --all
For private registries, configure node credentials or add image pull secrets as described in Private Registry and Image Pull Secrets.
Parent pod cannot find startup or local
The prepared kit was copied to the wrong level in the PVC, or the wrong PVC is mounted. The configured workspace mount path must contain:
<workspace_mount_path>/startup
<workspace_mount_path>/local
With the example default workspace_mount_path, those paths are:
/var/tmp/nvflare/workspace/startup
/var/tmp/nvflare/workspace/local
Use the helper pod from Workspace PVC to inspect /mnt/nvflws and restage
startup/ and local/ from the prepared folder.
Parent starts but cannot launch job pods
Check the parent logs for Kubernetes import or authorization failures:
kubectl -n "$NAMESPACE" logs deploy/server --tail=200
kubectl -n "$NAMESPACE" auth can-i create pods \
--as=system:serviceaccount:"$NAMESPACE":server
kubectl -n "$NAMESPACE" auth can-i create secrets \
--as=system:serviceaccount:"$NAMESPACE":server
If the logs show that the kubernetes Python package is missing, rebuild the
parent image with the NVFlare K8S extra or pip install kubernetes.
If the logs show SSLCertVerificationError with
CA cert does not include key usage extension, the parent Kubernetes client
is rejecting the cluster API-server CA. This is known to affect some MicroK8s
CA certificates that omit the X.509 keyUsage extension; see
canonical/microk8s#4864.
Regenerate or replace the cluster CA with an RFC 5280-compliant CA. As a
temporary compatibility workaround for development clusters, use a custom
parent image based on Python 3.12 or earlier. Do not disable Kubernetes API TLS
verification in production.
Job pod stays Pending or Unknown
When a submitted job cannot start because an SJ or CJ job pod remains
Pending or Unknown longer than job_launcher.pending_timeout seconds,
NVFLARE deletes the stuck pod and marks the job as
FINISHED:EXECUTION_EXCEPTION. Check cluster scheduling events:
kubectl -n "$NAMESPACE" get pods
kubectl -n "$NAMESPACE" describe pod <job-pod-name>
kubectl -n "$NAMESPACE" get events --sort-by=.lastTimestamp
Common causes include insufficient CPU, memory, GPU, or ephemeral storage; missing study-data PVCs; image pull failures; and missing GPU device-plugin resources.
Job pod cannot pull its image
Job pods use the image from the submitted job’s launcher_spec and set
imagePullPolicy: Always. Confirm the job image name and configure registry
credentials for dynamically launched pods. Use
job_launcher.image_pull_secrets in k8s.yaml for explicit Secret
references, or rely on node-level credentials or the namespace default
ServiceAccount if your cluster is configured that way.
Client cannot connect to the server
Verify these items:
default_hostinproject.ymlmatches the DNS name used by the client.The DNS name resolves from the client cluster.
The server cluster exposes
fed_learn_port.The server certificate includes the DNS name in
host_names.Network policy and firewalls allow outbound client traffic to the server.
Run a DNS check from the client cluster:
kubectl -n "$NAMESPACE" run dns-test --rm -it \
--image=busybox:1.36 -- \
nslookup server1.example.com
If you change default_host or host_names, reprovision, restage the
updated folders, and redeploy the charts.
Uninstall
To stop a participant installed by Helm:
helm uninstall server -n "$NAMESPACE"
helm uninstall site-1 -n "$NAMESPACE"
Delete the namespace only if it is dedicated to this deployment:
kubectl delete namespace "$NAMESPACE"
Depending on the storage class reclaim policy, PVC-backed volumes may remain after deleting Helm releases or namespaces. Remove retained volumes only after confirming that the startup kits, logs, snapshots, job history, and study data no longer need to be preserved.