Storage Requirements
The RunWhen Platform uses two kinds of storage:
- PersistentVolumeClaim (PVC) — for state that must survive pod restarts (Postgres, Vault, Neo4j, object store, Redis, Qdrant, Mimir).
- emptyDir — for scratch, caches, and working directories that can be recreated. Every first-party emptyDir has a
sizeLimitthat signals the kubelet to evict the pod if it exceeds the limit.
Required StorageClass
You need at least one default StorageClass (or a named class you configure per component). The chart works with any CSI driver that provides ReadWriteOnce volumes. For Postgres HA (Spilo) each replica needs its own PV — a non-shared block storage with low latency (SSD/NVMe) is strongly recommended.
| Scenario | StorageClass requirement | Notes |
|---|---|---|
| Production | Regional SSD-backed RWO | Postgres needs low-latency writes |
| NFS only | No RWO StorageClass available | Postgres on NFS is never supported — NFS lacks the POSIX locking and fsync semantics Postgres requires (see PostgreSQL and NFS). Use postgresql.spilo.persistence.kind: emptyDir with WAL-G enabled, or an external PostgreSQL. Other components can use emptyDir (see NFS-only cluster below). |
| OpenShift | Any provisioner | No special requirements beyond a working default StorageClass |
Per-component storage matrix
| Component | Backing store | Default size | Rebuildable? | Notes |
|---|---|---|---|---|
| Postgres (Spilo) | PVC (persistence.kind: pvc) | 10 Gi | Yes — WAL-G → S3 | Durability anchor. HA via Patroni. |
| Vault | PVC | 10 Gi | Yes — snapshot tarball → S3 | Durability anchor. Raft backend. |
| Neo4j | PVC (volumes.data.mode: defaultStorageClass) | 10 Gi | Yes — re-index from Postgres | Graph + vector index. Loss = re-index cost (minutes). |
| Object store (SeaweedFS) | PVC (volume server), PVC (filer) | 100 Gi volume, 5 Gi filer | No — durability anchor for Vault/Postgres backups | S3 gateway that backs all backup pipelines. |
| Redis | PVC (master.persistence.enabled: true) | 8 Gi | Yes — cache loss means cold-cache startup cost | Session store, cache, task queues. |
| Qdrant (subchart) | PVC (useSubchart: true) | 5 Gi | Yes — re-embed from Postgres/LLM | Vector DB for RAG. |
| MetricStore (Mimir) | PVC (persistence.kind: pvc) | 10 Gi | Yes — re-sync from S3 bucket store | TSDB blocks. |
| PgBouncer | emptyDir (socket) | 10 Mi limit | n/a | Unix socket only — negligible. |
Total PVC footprint
| Scope | Sum of default sizes |
|---|---|
| Full install | ~150 Gi (all components at default sizes) |
| Production | 500 Gi+ (SeaweedFS volume server dominates) |
NFS-only cluster
If your cluster has no RWO StorageClass (NFS is the only option), the following components must switch to emptyDir-backed storage because NFS is not suitable for their access patterns:
| Component | Setting | Impact |
|---|---|---|
| Postgres | postgresql.spilo.persistence.kind: emptyDir | High volume requirement. PGDATA is wiped on every pod restart. Recovery requires WAL-G → S3 base backup + WAL replay. The emptyDir sizeLimit must be set generously (default 10 Gi in the chart) to hold the database plus WAL accumulation between archive cycles. A stalled S3 backend can grow pg_wal/ quickly — monitor closely. For production, use an external PostgreSQL instead. |
| MetricStore (Mimir) | metricstore.persistence.kind: emptyDir | Mimir re-syncs TSDB blocks from S3 on startup. Slower first query after restart, then normal. |
| Qdrant | qdrant.useSubchart: false | Chart’s minimal Deployment on emptyDir. Full re-embed on restart. |
Example overlay for NFS-only clusters:
postgresql: spilo: persistence: kind: emptyDir size: 20Gi # generous — WAL accumulates between archives emptyDirMedium: "" # node disk, not tmpfs (tmpfs would be RAM-capped) walg: enabled: true # required — only recovery path when PGDATA is wiped archiveTimeout: 60 baseBackupSchedule: "0 */2 * * *"
metricstore: persistence: kind: emptyDir size: 10Gi
qdrant: useSubchart: false persistence: size: 10GiemptyDir scratch volumes
Every first-party pod has writable scratch volumes (/tmp + /home/app) backed by emptyDir with sizeLimit: 1Gi (configurable via global.scratchVolumes.sizeLimit). Additional component-specific emptyDirs handle binary copies, caches, and working directories.
What sizeLimit does (and doesn’t do)
sizeLimit on an emptyDir is an eviction signal, not a hard quota. The kubelet periodically checks usage and may evict the pod if it exceeds the limit. However:
- The check is periodic (default
--node-status-update-frequencyis 10s) — a pod can write well past the limit before the kubelet reacts. - The pod can consume as much node disk as is available until the kubelet evicts it.
- On managed Kubernetes (GKE, EKS, AKS) the eviction thresholds are often configured with long grace periods or relaxed entirely.
The real protection against node disk overrun requires a defense-in-depth approach — see Resource quotas for disk protection below.
Per-component emptyDir volumes
| Component | Volumes | sizeLimit | Est. actual usage |
|---|---|---|---|
| Vault (server) | vault-tmp | 1 Gi | ~5 MiB (config render) |
| Vault init | vault-bin | 100 Mi | ~70 MiB (binary copy) |
| Vault backup | vault-bin, tmp | 100 Mi, 1 Gi | ~80 MiB combined |
| Vault unsealer | vault-bin | 100 Mi | ~70 MiB (binary copy) |
| PgBouncer | socket | 10 Mi | <1 MiB |
| Spilo (Postgres) | spilo-run, spilo-tmp, spilo-varlog | 1 Gi each | ~50 MiB combined |
| LLM Gateway | litellm-tmp, litellm-cache, litellm-migrations, litellm-prisma-work | 1 Gi each | ~200 MiB combined |
| User Pages v2 | html | 100 Mi | ~50 MiB |
| CC Catalog | data (SQLite), tmp | 1 Gi each | ~30 MiB combined |
| Neo4j | neo4j-confdir, neo4j-run, neo4j-tmp | 100 Mi, 100 Mi, 1 Gi | ~150 MiB combined |
| SeaweedFS (each of 4 components) | tmp | 1 Gi each | ~100–500 MiB (multipart staging) |
| Global scratch (× 20 deployments) | platform-tmp + platform-home | 1 Gi each | ~50 MiB per pod |
Estimated total emptyDir footprint: ~3–5 GiB across all pods (vs. ~95 GiB sum of sizeLimits). The sizeLimit is an eviction ceiling, not a reservation.
Resource quotas for disk protection
Because emptyDir.sizeLimit is an eviction signal (not a hard quota), configure namespace-level ResourceQuota and per-container LimitRange with ephemeral-storage limits for write-time enforcement:
apiVersion: v1kind: ResourceQuotametadata: name: runwhen-platform-quotaspec: hard: # Cap total ephemeral storage across all pods in the namespace. # Adjust based on your node disk size and replica count. requests.ephemeral-storage: "50Gi" limits.ephemeral-storage: "100Gi" # PVC caps — match or exceed the per-component defaults. requests.storage: "200Gi" persistentvolumeclaims: 20 # Optional CPU / memory quotas. # requests.cpu: "32" # requests.memory: "128Gi" # limits.cpu: "64" # limits.memory: "256Gi"---apiVersion: v1kind: LimitRangemetadata: name: runwhen-platform-limitsspec: limits: - type: Container default: ephemeral-storage: "2Gi" # per-container hard limit defaultRequest: ephemeral-storage: "256Mi" # per-container request (scheduling) max: ephemeral-storage: "10Gi" # single container capOpenShift: auto-apply via ProjectRequestTemplate
On OpenShift, bake the quota and limit range into the cluster’s ProjectRequestTemplate so every new namespace gets them automatically:
apiVersion: template.openshift.io/v1kind: Templatemetadata: name: project-requestobjects: - apiVersion: project.openshift.io/v1 kind: Project metadata: name: ${PROJECT_NAME} - apiVersion: v1 kind: ResourceQuota metadata: name: platform-quota namespace: ${PROJECT_NAME} spec: hard: requests.ephemeral-storage: "50Gi" limits.ephemeral-storage: "100Gi" requests.storage: "200Gi" persistentvolumeclaims: 20 - apiVersion: v1 kind: LimitRange metadata: name: platform-limits namespace: ${PROJECT_NAME} spec: limits: - type: Container default: ephemeral-storage: "2Gi" defaultRequest: ephemeral-storage: "256Mi" max: ephemeral-storage: "10Gi"Verify the template is active:
oc get project.config.openshift.io/cluster -o jsonpath='{.spec.projectRequestTemplate.name}'If unset, apply and configure the cluster to use it:
oc apply -f project-request-template.yamloc patch project.config.openshift.io/cluster --type=merge \ -p '{"spec":{"projectRequestTemplate":{"name":"project-request"}}}'Related topics
| Topic | Link |
|---|---|
| Self-hosted prerequisites | Self-Hosted Deployment Requirements |
| External PostgreSQL | External PostgreSQL |
| External S3 | External S3 |
| OpenShift / OKD install | OpenShift & OKD — Self-Hosted |
| Pod security hardening | Security Hardening |