---
title: Add custom JARs using a persistent volume
sidebar_label: Add custom JARs to Kubernetes
---

Custom JARs are typically built into your container images (see [Kubernetes installation](./kubernetes-install-guide.md)). This guide shows how to add JARs without rebuilding your images: stage them on a persistent volume, mount that volume to pods, and add the mounted path to the classpath.

You can make the JARs available to system service pods (such as the Controller and the query and merge servers), to worker pods, or to both. Steps 1, 2, 3, and 5 apply in every case, because both volume mounts are Helm values that take effect only after the Helm upgrade in Step 5. Step 4 applies to system service pods, and Step 6 applies to workers.

## Step 1: Create a PersistentVolumeClaim

Create a PersistentVolumeClaim (PVC) for your custom JARs.

If you mount the volume to more than one pod (for example, both system services and workers), use a storage class that supports [ReadWriteMany (RWX)](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes). A ReadWriteOnce (RWO) class causes multi-attach errors when pods are scheduled on different nodes.

For RWX storage options, see [Configure shared storage](./kubernetes-install-guide.md#configure-shared-storage).

Create a file called `custom-jars-pvc.yaml`:

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: custom-jars-pvc
spec:
  accessModes:
    - ReadWriteMany # Required when pods on different nodes mount the volume
  storageClassName: your-rwx-storage-class # Must support RWX access mode
  resources:
    requests:
      storage: 1Gi
```

Apply it:

```bash
kubectl apply -f custom-jars-pvc.yaml
```

## Step 2: Stage JARs to the volume

Create a temporary pod to copy your JARs to the persistent volume. The pod's image must provide `sh`, `sleep`, and `tar`, because `kubectl cp` requires `tar`.

Create a file called `jar-staging-pod.yaml`:

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: jar-staging-pod
spec:
  containers:
    - name: staging
      image: busybox
      command: ["/bin/sh", "-c", "sleep infinity"]
      volumeMounts:
        - name: custom-jars
          mountPath: /jars
  volumes:
    - name: custom-jars
      persistentVolumeClaim:
        claimName: custom-jars-pvc
```

> [!TIP]
> If your cluster can't pull from Docker Hub, replace `busybox` with an image from a registry your cluster can reach that includes `sh`, `sleep`, and `tar`, and add any `imagePullSecrets` that registry requires. You can use the Deephaven management shell image if it includes those tools in your installation. Look up its name with `kubectl get deploy/management-shell -o jsonpath='{.spec.template.spec.containers[].image}'`.

Apply the pod and copy your JARs:

```bash
# Create the staging pod
kubectl apply -f jar-staging-pod.yaml

# Wait for the pod to be ready
kubectl wait --for=condition=Ready pod/jar-staging-pod

# Copy JARs to the volume
kubectl cp /local/path/to/your-library.jar jar-staging-pod:/jars/

# Make the JARs readable by all users
kubectl exec jar-staging-pod -- chmod -R a+r /jars

# Verify the files
kubectl exec jar-staging-pod -- ls -la /jars

# Delete the staging pod when done
kubectl delete pod jar-staging-pod
```

The `chmod` command is needed because the staging pod owns the copied files, while Deephaven processes run as other users.

## Step 3: Mount the volume to pods

Add one or both of the following sections to your Helm values override file (for example, `my-values.yaml`). To make the same JARs available to both system services and workers, use both.

### For system service pods

Use [`extraVolumes`](./kubernetes-configuration-settings.md#extravolumes) to mount the PVC to system service pods. These mounts are read-write.

<!-- dprint-ignore -->
```yaml
extraVolumes:
  - name: "custom-jars"
    claimName: "custom-jars-pvc"
    mountPath: "/custom/jars"
```

### For worker pods

Use [`workerExtraVolumes`](./kubernetes-configuration-settings.md#workerextravolumes) to mount the PVC to worker pods. For more on mounting data in workers, see [How do I make a certificate or other piece of external data available within my Persistent Queries?](./troubleshooting-kubernetes.md#how-do-i-make-a-certificate-or-other-piece-of-external-data-available-within-my-persistent-queries)

<!-- dprint-ignore -->
```yaml
workerExtraVolumes:
  - name: "custom-jars"
    claimName: "custom-jars-pvc"
    mountPath: "/custom/jars"
```

> [!NOTE]
> Query worker mounts are always read-only: because of a Helm template limitation, setting `readOnly: false` has no effect on them. Merge workers mount extra volumes read-write by default; to make a merge worker volume read-only, set `readOnly: true`.

## Step 4: Add the JARs to the system service classpath

If you mounted the volume to system service pods, add the mounted path to their classpath in the same values file. Use `classpathPre` to load the JARs before Deephaven classes, or `classpathPost` to load them after:

<!-- dprint-ignore -->
```yaml
process:
  common:
    classpathPre: "/custom/jars/*"
```

To configure a single process instead of all processes, use the process name in place of `common`:

<!-- dprint-ignore -->
```yaml
process:
  controller:
    classpathPre: "/custom/jars/*"
```

## Step 5: Apply the Helm configuration

[Scale down](./troubleshooting-kubernetes.md#how-do-i-scale-my-deephaven-deployment-down-or-up) your deployment and run a Helm upgrade with your values file, as described in [Install the Deephaven Helm chart](./kubernetes-upgrade-guide.md#install-the-deephaven-helm-chart):

```bash
./setupTools/scaleAll.sh down
helm upgrade --install <release-name> deephaven \
    -f my-values.yaml \
    --set image.tag=<your-deephaven-version>
```

## Step 6: Add the JARs to the worker classpath

If you mounted the volume to worker pods, add the mounted path to the worker classpath. Worker classpaths are set per Persistent Query or Code Studio, not in the Helm chart, so this step needs no Helm upgrade beyond the one in Step 5.

The volume mount from Step 3 and this classpath setting apply only to workers started after you make the change. Restart any Persistent Queries or Code Studios that were already running.

In the **Show Advanced** section of the [Persistent Query](../../query-management/ui-queries.md#show-advanced) or [Code Studio](../../interfaces/web/code-studio.md#show-advanced) settings, add the mounted path to the [**Extra Classpaths**](../../query-management/worker-configuration.md#extra-classpaths) field. This works for both Legacy and Core+ workers:

```text
/custom/jars/*
```

> [!NOTE]
> For Core+ workers, you can instead set `DND_EXTRA_CLASSPATH=/custom/jars/*` in **Extra Env Variables**. A `DND_EXTRA_CLASSPATH` value set there replaces anything entered in **Extra Classpaths**, so use one or the other.

## Related documentation

- [Kubernetes installation guide](./kubernetes-install-guide.md)
- [Kubernetes upgrade guide](./kubernetes-upgrade-guide.md)
- [Kubernetes configuration settings](./kubernetes-configuration-settings.md)
- [Kubernetes process launch configuration](./kubernetes-process-launch.md#classpath-and-custom-libraries)
- [Customize your Kubernetes installation](./kubernetes-customizing-installation.md)
- [Kubernetes troubleshooting](./troubleshooting-kubernetes.md)
