Harthelm Docs
Configure

Storage

Workspace volumes: the two storage modes, what a StorageClass must offer, the workspace UID, and the other volumes.

Last updated 2026-10-19

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:

ModeVolumePlacementFits
rwxone ReadWriteMany volumeanywhereNFS-style storage (csi-driver-nfs, EFS, Filestore through NFS, Azure Files NFS, Longhorn RWX)
rwo-colocatedone ReadWriteOnce volumeevery pod of the workspace on the node of its volumenode-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.

my-values.yaml
storage:
  class: workspaces-nfs
  mode: rwx
  defaultSizeGiB: 10

storage.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 driver chmod each volume after mounting. The tested setup exports with no_root_squash.
  • A fresh ext4 or xfs block volume is root:root 0755 and does not work for workspace volumes. This rules out EBS, Persistent Disk and Azure Disk for rwx; rwo-colocated on 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:

  1. Upgrade with security.uidGuard.allowChangeFrom set to the old UID. New volumes get the new UID; existing pods keep their volume's.
  2. 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

VolumeClassNotes
The bundled platform databasedatabase.bundled.storage.storageClass, database.bundled.storage.size (5Gi)Any ReadWriteOnce class; CloudNativePG sets its own fsGroup.
Users' PostgreSQL databasesmarketplace.builtin.postgres.storageClassA class with reclaimPolicy: Delete fits: deleting a database deletes its data, and the nightly dumps are the copy.
Users' MariaDB, Valkey, RabbitMQ and S3 instancesmarketplace.builtin.mariadb.storageClass, .valkey.storageClass, .rabbitmq.storageClass, .s3.storageClassReadWriteOnce. 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.
Backupsbackup.persistence (5Gi, ReadWriteOnce) or backup.persistence.existingClaimKept 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.

On this page