Storage
Workspace volumes: the two storage modes, what a StorageClass must offer, the workspace UID, and the other volumes.
Every workspace has one volume. It holds the code (/data/app) and the persisted tools (/data/tools: Nix, mise,
the IDE's settings, the shell), and it survives sleep. The IDE and every service of the workspace mount it.
Choose a mode
storage.mode decides how the pods of a workspace share its volume:
| Mode | Volume | Placement | Fits |
|---|---|---|---|
rwx | one ReadWriteMany volume | anywhere | NFS-style storage (csi-driver-nfs, EFS, Filestore through NFS, Azure Files NFS, Longhorn RWX) |
rwo-colocated | one ReadWriteOnce volume | every pod of the workspace on the node of its volume | node-local disks (TopoLVM, local-path) and zonal disks; fast for many small files |
auto (default) | Core probes storage.class at start: a WaitForFirstConsumer class counts as ReadWriteOnce; otherwise a small ReadWriteMany test claim must bind within 30 seconds for rwx; else rwo-colocated. |
Set the mode explicitly once you know your class: some ReadWriteMany classes bind only on first use, and auto reads
those as ReadWriteOnce.
storage:
class: workspaces-nfs
mode: rwx
defaultSizeGiB: 10storage.defaultSizeGiB is the size for plans that set no storageGiB. A volume keeps its class, mode and owner:
changing these values affects new workspaces only.
Network storage is much slower than a local disk for many small files (package installs, git status on a large
tree). For installs with heavy development use, prefer rwo-colocated on local disks; then a workspace's pods cannot
move to another node, and a node that is down takes its workspaces with it until it is back.
What the class must offer
Workspace pods run as one non-root UID with group 0 and no fsGroup: Harthelm never changes ownership recursively
at start. So the root of a fresh volume must be writable by group 0 or by everyone.
- NFS through csi-driver-nfs: set
mountPermissions: "0775", which makes the driverchmodeach volume after mounting. The tested setup exports withno_root_squash. - A fresh ext4 or xfs block volume is
root:root 0755and does not work for workspace volumes. This rules out EBS, Persistent Disk and Azure Disk forrwx;rwo-colocatedon a node-local provisioner works. - SMB shares take ownership from mount options and do not fit.
If the class is wrong, workspaces never get past their first start: the seed cannot create /data/app.
The workspace UID
security.defaultUid (default 1001) is the UID workspace pods run as, and it is written into every file on every
workspace volume. Core records the UID the volumes belong to in the ConfigMap <release>-volume-uid, and
helm upgrade refuses a different defaultUid while volumes exist (security.uidGuard).
To change it on purpose:
- Upgrade with
security.uidGuard.allowChangeFromset to the old UID. New volumes get the new UID; existing pods keep their volume's. - As a cluster admin, re-own the volumes of each team with
POST /teams/{team}/volume-owner(REST API, tag Teams). Core runs one short root Job per volume and records the new UID when the last volume is done.
helm template and Argo CD cannot read the ConfigMap; there core's own check at start is the guard, and
security.uidGuard.state can feed the chart's by hand.
The other volumes
| Volume | Class | Notes |
|---|---|---|
| The bundled platform database | database.bundled.storage.storageClass, database.bundled.storage.size (5Gi) | Any ReadWriteOnce class; CloudNativePG sets its own fsGroup. |
| Users' PostgreSQL databases | marketplace.builtin.postgres.storageClass | A class with reclaimPolicy: Delete fits: deleting a database deletes its data, and the nightly dumps are the copy. |
| Users' MariaDB, Valkey, RabbitMQ and S3 instances | marketplace.builtin.mariadb.storageClass, .valkey.storageClass, .rabbitmq.storageClass, .s3.storageClass | ReadWriteOnce. The class must apply the pods' fsGroup or hand out volumes their UID can write; NFS servers with manage-gids work. With allowVolumeExpansion the volumes can grow; without it, plans that need more storage are refused. |
| Backups | backup.persistence (5Gi, ReadWriteOnce) or backup.persistence.existingClaim | Kept when the release is deleted. On a class whose root is not group-writable, set backup.fsGroup. |
Empty classes mean the cluster's default StorageClass. The preflight Job checks that every class the values name exists.