# JupyterHub with K8s: Shared /home volume?

**URL:** <https://discourse.jupyter.org/t/jupyterhub-with-k8s-shared-home-volume/35691>\
**Category:** Zero to JupyterHub on Kubernetes\
**Tags:** help-wanted\
**Created:** [May 29, 2025, 5:25pm UTC](https://discourse.jupyter.org/t/jupyterhub-with-k8s-shared-home-volume/35691 "2025-05-29T17:25:01Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![ana-v-espinoza](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.jupyter.org/ana-v-espinoza/32/13722_2.png) [@ana-v-espinoza](https://discourse.jupyter.org/u/ana-v-espinoza)\
**Post date:** [May 29, 2025, 5:25pm UTC](https://discourse.jupyter.org/t/jupyterhub-with-k8s-shared-home-volume/35691/1 "2025-05-29T17:25:01Z")

</div>

Hello Jovyans,

I’m trying to figure out a way to have `/home` exist as a share volume mount, and have users’ home directories, i.e. `/home/$USER`, all be found on this mount.

Here’s my motivation. I want to:

1. use fewer volumes (provided by the underlying cloud), as all user data will exist on a single volume as opposed to one volume per user
2. simplify volume backups/snapshots; 1 vs many
3. allow JHub admins (in my case, classroom instructors) to more easily access student files for grading of assignments, while students cannot peak at each others’ work

* * *

Roughly, I see a path forward to get me most of the way there:

1. Export an NFS volume from a dedicated NFS server Pod
2. Use the [extraVolumeMounts and extraVolumes](https://z2jh.jupyter.org/en/latest/resources/reference.html#singleuser-storage-extravolumemounts) to mount this shared volume at `/home` for each single user server
3. Use the [hub.extraConfig](https://z2jh.jupyter.org/en/latest/resources/reference.html#hub-extraconfig) to subclass kubespawner and:
  - define a function that returns some necessary environment variables as a dictionary\*
  - set [KubeSpawner.environment](https://jupyterhub-kubespawner.readthedocs.io/en/latest/spawner.html#kubespawner.KubeSpawner.environment) to what’s returned by this function

4. Start the single user container as `root`

\* The necessary environment variables are:

- `NB_USER = <jhub-username>`
- `NB_GROUP = "users"` or `NB_GROUP = "admin"` (for JHub administrators)
- `NB_UID = <some unique uid>`
- `NB_GID = "100"` (users) or `NB_GROUP = "200"` (admin)

The last two steps are so that this section of [start.sh](https://github.com/jupyter/docker-stacks/blob/08cc9f7d17896a8d9653c14731b670ab62755498/images/docker-stacks-foundation/start.sh#L57), provided as part of jupyter-docker-stacks, will run appropriately and start the jupyterlab server as the appropriate user, uid, group, gid, and with home directory (in `/home/$NB_USER`).

I’m sure there’s some unintentional hand-waving in the steps I’ve described.

* * *

The part that I’m having trouble figuring out is how to give users `<some unique uid>`, which will allow us to then set home directory ownership to `<uid>:200` (i.e. `<user>:admin`) and permission to `770` (rwx by `<user>` and admins, inaccessible by everybody else).

My gut tells me that I should store user `uid`’s in the JHub data base (specifically in each user’s Spawner state). My function that returns the `KubeSpawner.environment` would then have to either read this value from the data base or, if it doesn’t exist, create the next available `uid`. I don’t know how to do this!

After reading through the docs and some the source for JupyterHub and Kubespawner, I’ve decided that I should reach out for help since I’m having trouble understanding how data gets to/from the database and the spawner instances.

* * *

To be explicit in what my questions are:

1. First of all, based on my motivations, is having a shared `/home` directory an appropriate solution?
2. Is this an appropriate implementation?
3. If yes to the above, how can I interact with the JHub database to create/read unique user `uid`’s?

Thanks!

ana v e

---

<div class="post-metadata">

**Author:** ![mahendrapaipuri](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.jupyter.org/mahendrapaipuri/32/6800_2.png) [@mahendrapaipuri](https://discourse.jupyter.org/u/mahendrapaipuri)\
**Post date:** [May 30, 2025, 6:53am UTC](https://discourse.jupyter.org/t/jupyterhub-with-k8s-shared-home-volume/35691/2 "2025-05-30T06:53:48Z")

</div>

> [@ana-v-espinoza](#):
>
> My gut tells me that I should store user `uid`’s in the JHub data base (specifically in each user’s Spawner state). My function that returns the `KubeSpawner.environment` would then have to either read this value from the data base or, if it doesn’t exist, create the next available `uid`. I don’t know how to do this!

Which authenticator are you using and which is the identity provider? Ideally this sort of user related information should come from IdP. I am assuming your IdP does not store/provide `uid` for the users. In this case, creating `uid` on the authenticator and passing it to spawner via `authstate` can be solution. You can save the created `uid` info in a simple text file or a SQLite DB in the `post_auth_hook` of Authenticator.

---

<div class="post-metadata">

**Author:** ![ana-v-espinoza](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.jupyter.org/ana-v-espinoza/32/13722_2.png) [@ana-v-espinoza](https://discourse.jupyter.org/u/ana-v-espinoza)\
**Post date:** [May 30, 2025, 3:37pm UTC](https://discourse.jupyter.org/t/jupyterhub-with-k8s-shared-home-volume/35691/3 "2025-05-30T15:37:50Z")

</div>

Mahendra,

Thanks for the response!

We’re using GitHub OAuth.

Indeed, your suggestion seems simpler than my approach. I’ll be looking further into this and will share out when I have something of substance.

Best,

ave

---

<div class="post-metadata">

**Author:** ![tony](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.jupyter.org/tony/32/7642_2.png) [@tony](https://discourse.jupyter.org/u/tony)\
**Post date:** [June 5, 2025, 1:37am UTC](https://discourse.jupyter.org/t/jupyterhub-with-k8s-shared-home-volume/35691/4 "2025-06-05T01:37:48Z")

</div>

Hi Ava,

I do something similar as to what I think you are asking. First I create a PV then a PVC in the namespace, this is my PVC YAML

```auto
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: fcba-su25-pvc
spec:
  storageClassName: fcba-su25-sc
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Gi

```

Then in the jupyterhub config I have:

```auto
  storage:
    type: static
    static:
      pvcName: 'fcba-su25-pvc'
      subPath: '{username}'

```

All users files are under one folder in my NFS mounted storage on the cluster. For example:

/home/fcba-su25/user1  
/home/fcba-su25/user2  
/home/fcba-su25/user3

The folders are automatically created when users login. I have used this method with github auth and google oauth2.

This may or not be the correct way to do it, but it works well for me.

Tony

---

<div class="post-metadata">

**Author:** ![ana-v-espinoza](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.jupyter.org/ana-v-espinoza/32/13722_2.png) [@ana-v-espinoza](https://discourse.jupyter.org/u/ana-v-espinoza)\
**Post date:** [June 5, 2025, 4:55pm UTC](https://discourse.jupyter.org/t/jupyterhub-with-k8s-shared-home-volume/35691/5 "2025-06-05T16:55:10Z")

</div>

Hey Tony,

Thanks for the feedback. This seems like a handy method and I’ll give it a try when I have a chance. I assume, however, that each of these directories are all owned by the jovyan user, as is the nature of the JupyterLab containers. Would this not give users `rwx` permissions on each others files? The motivation behind giving users their own `uid` is to prevent this.

Best,

ana v e

---

<div class="post-metadata">

**Author:** ![tony](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.jupyter.org/tony/32/7642_2.png) [@tony](https://discourse.jupyter.org/u/tony)\
**Post date:** [June 5, 2025, 5:22pm UTC](https://discourse.jupyter.org/t/jupyterhub-with-k8s-shared-home-volume/35691/6 "2025-06-05T17:22:07Z")

</div>

Hi Ana,

When I started this method, I had Yuvi (Zero-to-Jupyterhub developer) look at it and he did not see any security issues. Check this out if I open a terminal:

```auto
jovyan@jupyter-montereytony-40gmail-2ecom:~$ pwd
/home/jovyan
jovyan@jupyter-montereytony-40gmail-2ecom:~$ cd ..
jovyan@jupyter-montereytony-40gmail-2ecom:/home$ ls
jovyan
jovyan@jupyter-montereytony-40gmail-2ecom:/home$ cd /
jovyan@jupyter-montereytony-40gmail-2ecom:/$ ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
jovyan@jupyter-montereytony-40gmail-2ecom:/$ cd
jovyan@jupyter-montereytony-40gmail-2ecom:~$ pwd
/home/jovyan
jovyan@jupyter-montereytony-40gmail-2ecom:~$

```

I do not see anyone else’s folder.

Tony

---

<div class="post-metadata">

**Author:** ![ana-v-espinoza](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.jupyter.org/ana-v-espinoza/32/13722_2.png) [@ana-v-espinoza](https://discourse.jupyter.org/u/ana-v-espinoza)\
**Post date:** [June 5, 2025, 7:02pm UTC](https://discourse.jupyter.org/t/jupyterhub-with-k8s-shared-home-volume/35691/7 "2025-06-05T19:02:51Z")

</div>

Tony,

Excellent! I think I understand what is going on here now. The subPath of the PVC is being mounted and not the full volume (with _every_ user’s data) itself?

Thanks again for the input, I’ll be looking more into this to make sure I see the full picture.

ana v e

---

<div class="post-metadata">

**Author:** ![tony](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.jupyter.org/tony/32/7642_2.png) [@tony](https://discourse.jupyter.org/u/tony)\
**Post date:** [June 5, 2025, 7:36pm UTC](https://discourse.jupyter.org/t/jupyterhub-with-k8s-shared-home-volume/35691/8 "2025-06-05T19:36:37Z")

</div>

Hey Ana,

I am not sure of the terminology, best to wait for experts to chime in. But for sure, the users can only see their own data.

Tony

---

<div class="post-metadata">

**Author:** ![Paul2708](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.jupyter.org/paul2708/32/14207_2.png) [@Paul2708](https://discourse.jupyter.org/u/Paul2708)\
**Post date:** [June 6, 2025, 12:15pm UTC](https://discourse.jupyter.org/t/jupyterhub-with-k8s-shared-home-volume/35691/9 "2025-06-06T12:15:50Z")

</div>

> [@ana-v-espinoza](#):
>
> The subPath of the PVC is being mounted and not the full volume (with _every_ user’s data) itself?

You are right. We use the same mechanism to manage submissions: The lecturer receives the “full volume” (i.e., a directory with sub-directories for each student), and the students receive the volume with a subPath (i.e., their username).

---

<div class="post-metadata">

**Author:** ![ana-v-espinoza](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.jupyter.org/ana-v-espinoza/32/13722_2.png) [@ana-v-espinoza](https://discourse.jupyter.org/u/ana-v-espinoza)\
**Post date:** [June 6, 2025, 9:41pm UTC](https://discourse.jupyter.org/t/jupyterhub-with-k8s-shared-home-volume/35691/10 "2025-06-06T21:41:21Z")

</div>

Thanks for the help everybody!

I’ve successfully been able to get a similar setup working in our cluster. Next week I’ll reply with a more substantial description for others to have a look at and replicate if they come across this thread. In short, I’ve:

- Create a cloud backed PVC that will act as the shared storage
- Create an NFS server Deployment and Service; I followed one of [K8s archived examples](https://github.com/kubernetes/examples/blob/0598f0762a905fd87d19891e3f084cd1a43559b8/_archived/volumes/nfs/README.md) adapted to my needs to achieve this
- Create a PV of NFS type
- In the same namespace as my JupyterHub, create a PVC that binds to the above PV
- Edit my `values.yaml` to have every Pod mount a subPath of the above PVC

There are still a few things I’ve yet to figure out:

1. Tony’s example shows a storage request of `10Gi`. Does this correspond to an enforced storage quota? I suppose this is easy enough to test myself!

2. How does one give instructor/admin-only access to the full volume? In principle, it’s as easy as mounting it as an [extraVolume](https://z2jh.jupyter.org/en/latest/resources/reference.html#singleuser-storage-extravolumes) without the subPath. Is this accomplished by modifying Kubespawner to configure the extra volume (and volumeMount) when the authenticated user is an admin? Or is this yet another case of me over-complicating things?

Best,

ana v e

---

<div class="post-metadata">

**Author:** ![manics](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.jupyter.org/manics/32/85_2.png) [@manics](https://discourse.jupyter.org/u/manics)\
**Post date:** [June 7, 2025, 2:02pm UTC](https://discourse.jupyter.org/t/jupyterhub-with-k8s-shared-home-volume/35691/11 "2025-06-07T14:02:03Z")

</div>

NFS quotas on Kubernetes depend on the NFS implementation, many ignore the quota request and give you unlimited storage.

Adding a conditional extra volume for instructors should work. [`group_overrides`](https://jupyterhub-kubespawner.readthedocs.io/en/7.0.0/spawner.html#kubespawner.KubeSpawner.group_overrides) might work, if not you should be able to add the volume using [`modify_pod_hook`](https://jupyterhub-kubespawner.readthedocs.io/en/7.0.0/spawner.html#kubespawner.KubeSpawner.modify_pod_hook)
