Skip to content

Self-Hosted Deployment Requirements

Self-hosted means you run the RunWhen Platform inside Kubernetes clusters you own, with task execution from RunWhen Local runners deployed into the same or separate clusters. RunWhen ships the platform as a Helm chart (runwhen-platform) and supplies a per-customer registry credential; everything else — clusters, DNS, registry, LLM, secrets — stays in your environment.

This page is for architecture, security, and platform-engineering reviewers evaluating self-hosted before procurement. If you are mid-install, your onboarding pack contains the version-pinned runbook; this page is the durable reference.

Start by contacting RunWhen

Self-hosted is not a public open-source install. The platform images and the OCI Helm chart live in a private Google Artifact Registry repository that requires a per-customer credential issued by RunWhen.

Before procurement, infrastructure design, or registry setup, contact support@runwhen.com or sales@runwhen.com to:

ItemWhat you receive
Self-hosted entitlementLicense terms, support tier, chart version channel, upgrade cadence
Registry credentialGCP service-account JSON (or Workload Identity Federation config) scoped read-only to your dedicated runwhen-self-hosted/<repo>
Onboarding packImage inventory with pinned tags, JFrog apply.sh (Pro/Enterprise), values-example-*.yaml overlays, install checklist, verification scripts
Companion image accessAnonymous ghcr.io/runwhen-contrib/* for MCP server, code-collection catalog, cortex-tenant, and codecollections

RunWhen retains the right to revoke the registry credential. The credential is scoped to a single Artifact Registry repository — it cannot read any other RunWhen registry, and it is the only path to private images.


Architecture overview

High-level architecture: RunWhen Platform cluster, RunWhen Local runner clusters, enterprise registry, customer DNS, and customer-supplied LLM — all inside the customer network with outbound-only flows.

Trust boundaries at a glance

BoundaryOwned byCrossesNotes
Platform clusterYouBrowser HTTPS in, registry pulls out, LLM outHosts UI, PAPI, AgentFarm, search index, Vault, Postgres, Neo4j, object store
Runner cluster(s)YouOutbound mTLS to the platformWhere every task executes; no inbound ports
Systems under automationYouReached only by runner workersCloud APIs, databases, queues, application services
Enterprise registryYouPulls upstream once; serves cluster pullsRunWhen, contrib, and subchart images flow through here for Paths B/C
LLM endpointYouReached via the in-cluster LiteLLM gatewayAzure OpenAI, vLLM, Bedrock, OpenAI, etc.
RunWhenRunWhenOne-time credential delivery + upgrade tagsNo persistent inbound or data-path connection from RunWhen into your environment

What the platform cluster runs

LayerComponents
EdgeIngress controller, cert-manager, optional MCP server
ApplicationUI, PAPI, AgentFarm, runner-control, USearch + Sobow, LiteLLM gateway, webhooks
StatefulPostgres (Spilo HA + PgBouncer, or external PostgreSQL), Neo4j (graph + vectors), Vault (auto-unseal), Object store (SeaweedFS or external S3), Redis
Cluster prereqsStorageClass, DNS, ingress, cert-manager (required for runner mTLS)

What runner clusters run

ComponentPurpose
workspace-builderDiscovers in-cluster resources, syncs config to the platform
runner + workersExecute tasks (bash, Python). Credentials and raw infrastructure data stay in the runner cluster

Runner clusters initiate connections outbound only to the platform’s papi, runner-control, and Vault hostnames over HTTPS / mTLS.

A single runner can discover and operate against many Kubernetes clusters and cloud environments. Placement is bounded by network reachability, not cluster count: deploy one runner per network segment that can reach the resources you want it to manage. Most customers run two or three (e.g. one per VPC / region) rather than one per discovered cluster.


Choose your install path

Three patterns satisfy most enterprise registry policies. The Helm chart, namespace, and Kubernetes resources are identical across all three; only where clusters pull images differs.

PathWhen to pick itCluster outboundMirror outbound
A — Direct pullCluster has egress to *.docker.pkg.dev and accepted public registriesRunWhen GAR, GHCR, Docker Hubn/a
B — Enterprise mirrorCorporate policy mandates internal registry pulls (most enterprises)Your registry onlyOne-time, scheduled, or pull-through
C — Air-gappedRegulated, classified, or fully disconnected networksNone (your registry only)Manual transfer across the gap

Path B is the most common enterprise choice. JFrog Artifactory is covered in detail in the JFrog Registry Setup guide; Google Artifact Registry is covered in the GAR Registry Setup guide. Harbor, ECR pull-through, and Nexus follow the same model with different specifics.

Plan the registry first. Procurement of registry capacity, vulnerability-scanning policy, and upstream-FQDN egress approvals tend to be the slowest items on enterprise self-hosted timelines.


Kubernetes prerequisites

These must exist before helm install. The chart assumes a production-grade cluster; single-node lab clusters are supported with reduced replica counts via the values-example-single-node.yaml overlay.

Required platform services

PrerequisiteWhy it matters
Kubernetes 1.28+Tested baseline; chart dependencies require modern API versions
OpenShift / OKD (if applicable)SCC and cluster-scoped RBAC require cluster-admin steps before Helm — see OpenShift / OKD self-hosted guide
StorageClass (default or per-component)Backs Postgres, Neo4j, Vault, object store, and vector DB PVCs
cert-manager (Ready)Hard dependency — runner mTLS certificates are minted at registration time, regardless of how ingress TLS is solved
Ingress controllerRoutes app., papi., agentfarm., runner-control., vault., and related hostnames
DNSglobal.domain and service subdomains resolve to the ingress load balancer

The platform uses emptyDir scratch volumes for caches, working directories, and binary copies. While the chart sets sizeLimit on every emptyDir, this is an eviction signal — the kubelet will not block writes at the moment the limit is exceeded (see emptyDir scratch volumes for details).

To protect against node disk overrun, configure a ResourceQuota and LimitRange in the install namespace before deploying the chart:

apiVersion: v1
kind: ResourceQuota
metadata:
name: runwhen-platform-quota
spec:
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 below.
requests.storage: "200Gi"
persistentvolumeclaims: 20
# CPU / memory quotas (optional — uncomment if you need them).
# requests.cpu: "32"
# requests.memory: "128Gi"
# limits.cpu: "64"
# limits.memory: "256Gi"
---
apiVersion: v1
kind: LimitRange
metadata:
name: runwhen-platform-limits
spec:
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 cap

On OpenShift, bake these into the cluster’s ProjectRequestTemplate so every new namespace gets them automatically — see the Resource quotas for disk protection section for the template YAML.

Workstation tools

ToolMinimum versionUsed for
Helm3.13+Chart pull, dependency materialization, install/upgrade
kubectl1.28+ (matching cluster)Pull secret, namespace, verification
Docker or crane / skopeoCurrentRegistry login; required for air-gap image transfer

Helm subchart materialization

The released chart is thin. Your install workstation registers five Helm-repo aliases before helm dependency update. Names must match exactly — Chart.yaml references them as @bitnami, @neo4j, @hashicorp, @qdrant, @seaweedfs.

AliasConnected (Path A)Mirrored (Paths B/C)
bitnamihttps://charts.bitnami.com/bitnamihttps://<registry>/helm-bitnami
neo4jhttps://helm.neo4j.com/neo4jhttps://<registry>/helm-neo4j
hashicorphttps://helm.releases.hashicorp.comhttps://<registry>/helm-hashicorp
qdranthttps://qdrant.github.io/qdrant-helmhttps://<registry>/helm-qdrant
seaweedfshttps://seaweedfs.github.io/seaweedfs/helmhttps://<registry>/helm-seaweedfs

The RunWhen chart itself is an OCI artifact in the same private registry as the platform images — pulled with helm pull oci://..., not via a classic Helm repo index.


Enterprise registry requirements

For Paths B and C, the enterprise container registry is the gating prerequisite. Treat this section as the minimum bar for security and platform review.

Why an enterprise registry

  • Policy compliance — clusters never pull directly from the public internet
  • Vulnerability scanning — images scanned and approved before production clusters see them
  • Air-gap readiness — the same mirror workflow supports fully disconnected installs
  • Credential isolation — the RunWhen pull credential lives on the mirror / CI host, not on every node
  • Tag pinning — point your overlays at locally-cached tags so RunWhen registry availability never gates your deploys

Upstream registries to proxy or mirror

UpstreamRequired?ContentsAuth
us-docker.pkg.devYesRunWhen platform images + OCI Helm chart (runwhen-self-hosted/platform-images/**)RunWhen-issued GCP SA
ghcr.ioYesrunwhen-contrib/** — MCP, catalog, cortex-tenant, codecollections, LiteLLM, SpiloAnonymous
registry-1.docker.ioYesSubchart images (Vault, Redis, Mimir, Qdrant, Neo4j, SeaweedFS)Anonymous (creds optional)
Helm chart reposConditionalcharts.bitnami.com, helm.neo4j.com, helm.releases.hashicorp.com, qdrant.github.io, seaweedfs.github.ioAnonymous
quay.io, registry.k8s.ioOptionalcert-manager and cluster-prerequisite images if you proxy them tooAnonymous

Registry routing in Helm values

How you point the chart at your mirror depends on layout:

Mirror layoutPatternExample
Single host, flat namespace (Harbor project, ECR pull-through, generic Distribution)registryOverride (chart-wide)harbor.corp/runwhen/<image>:<tag>
Per-upstream paths (JFrog virtual repos, Harbor with one project per upstream)Per-image registry: overrides<jcr-host>/docker/<remote>/<upstream-path>:<tag>

Worked overlays for both patterns ship in your onboarding pack. JFrog customers have a dedicated setup guide.

Cluster pull secret

Create a kubernetes.io/dockerconfigjson Secret in the install namespace. The chart default expects the name gcr-pull-secret (images.pullSecrets[0].name). Override in values if your naming policy requires a different name.

PathPull-secret authenticates againstCredential
A*.docker.pkg.devRunWhen-issued GCP SA JSON (_json_key username)
B / CYour enterprise registryRead-scoped JFrog / Harbor / ECR token

Code-collection container images

The runner pulls per-task code-collection containers at dispatch time. The in-cluster cc-catalog-svc discovers tags via the OCI Distribution tags/list API — your ghcr.io mirror must allow listing remote folder items (a critical JFrog checkbox; equivalent setting on Harbor/Nexus). Tags roll on RunWhen’s release schedule; pre-scan the baseline tags from your onboarding inventory and re-scan on each release if your security pipeline requires it.


Cluster sizing

These are starting points; RunWhen validates final sizing during onboarding based on workspace size and concurrent runner load.

Proof of concept / evaluation

ResourceMinimum
Kubernetes1.28+, 3 worker nodes
Per node4 vCPU, 16 GiB RAM
Storage~410 GiB allocatable PVC capacity — see Storage Requirements for per-component sizing
Postgres / RedisChart-managed (bundled subcharts)
NetworkOne ingress LB IP, DNS for global.domain
ResourceMinimum
Kubernetes1.28+, 6+ worker nodes
Per node8 vCPU, 32 GiB RAM
Storage500 GiB+ SSD-backed StorageClass — see Storage Requirements for per-component sizing and NFS guidance
PostgresSpilo HA + PgBouncer (overlay in onboarding pack)
RedisBitnami HA
NetworkDedicated ingress LB; consider PodAntiAffinity for stateful subcharts

Resource quotas: At this scale, set requests.ephemeral-storage: "100Gi" and limits.ephemeral-storage: "200Gi" in your ResourceQuota, and add CPU/memory quotas (requests.cpu: "48", requests.memory: "192Gi") to prevent one team’s run-away query or workspace from starving the rest of the platform. See Storage Requirements — Resource quotas for the full YAML.

See also Deployment Options — Option 3 for a feature-level SaaS-vs-self-hosted comparison.


Storage requirements

The 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 sizeLimit that 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 (SSD/NVMe) is strongly recommended.

ScenarioStorageClass requirementNotes
NFS onlyNo RWO StorageClass availableSet postgresql.spilo.persistence.kind: emptyDir, metricstore.persistence.kind: emptyDir, qdrant.useSubchart: false. State rebuilds on pod restart. See values-example-storage-nfs.yaml
ProductionRegional SSD-backed RWOPostgres needs low-latency writes; NFS for Postgres causes data-loss risk (see PostgreSQL and NFS)
OpenShiftAny provisionerNo special requirements beyond a working default StorageClass

Per-component storage matrix

ComponentBacking storeDefault sizeRebuildable?Notes
Postgres (Spilo)PVC (persistence.kind: pvc)10 GiYes — WAL-G → S3Durability anchor. HA via Patroni. kind: emptyDir for NFS-only clusters requires WAL-G enabled.
VaultPVC10 GiYes — snapshot tarball → S3Durability anchor. Raft backend; the PVC holds both data and Raft log.
Neo4jPVC (volumes.data.mode: defaultStorageClass)10 GiYes — re-index from PostgresGraph + vector index. Loss = re-index cost (minutes).
Object store (SeaweedFS)PVC (volume server), PVC (filer)100 Gi volume, 5 Gi filerNo — durability anchor for Vault/Postgres backupsThe S3 gateway that backs all backup pipelines.
RedisPVC (master.persistence.enabled: true)8 GiYes — cache loss means cold-cache startup costSession store, cache, task queues.
Qdrant (subchart)PVC (useSubchart: true)5 GiYes — re-embed from Postgres/LLMVector DB for RAG. useSubchart: false + NFS = emptyDir, full re-embed on restart.
MetricStore (Mimir)PVC (persistence.kind: pvc)10 GiYes — re-sync from S3 bucket storeTSDB blocks. kind: emptyDir for NFS-only clusters.
PgBounceremptyDir (socket)10 Mi limitn/aUnix socket only — negligible.

emptyDir 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.

ComponentVolumessizeLimitEst. actual usage
Vault (server)vault-tmp1 Gi~5 MiB (config render)
Vault initvault-bin100 Mi~70 MiB (binary copy)
Vault backupvault-bin, tmp100 Mi, 1 Gi~80 MiB combined
Vault unsealervault-bin100 Mi~70 MiB (binary copy)
PgBouncersocket10 Mi<1 MiB
Spilo (Postgres)spilo-run, spilo-tmp, spilo-varlog1 Gi each~50 MiB combined
LLM Gatewaylitellm-tmp, litellm-cache, litellm-migrations, litellm-prisma-work1 Gi each~200 MiB combined
User Pages v2html100 Mi~50 MiB
CC Catalogdata (SQLite), tmp1 Gi each~30 MiB combined
Neo4jneo4j-confdir, neo4j-run, neo4j-tmp100 Mi, 100 Mi, 1 Gi~150 MiB combined
SeaweedFS (each of 4 components)tmp1 Gi each~100–500 MiB (multipart staging)
Global scratch (× 20 deployments)platform-tmp + platform-home1 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.

SeaweedFS master logs: The chart ships with seaweedfs.master.logs.type: "", which directs master logs to stderr (captured by the container runtime) instead of a file volume. If you override to "emptyDir", the upstream subchart creates an unbounded emptyDir that requires a Helm post-renderer or Kustomize patch to add a sizeLimit.

Resource quotas for disk protection

emptyDir.sizeLimit is an eviction signal, not a hard quota. For write-time enforcement against node disk overrun, configure namespace-level ResourceQuota and per-container LimitRange with ephemeral-storage limits:

apiVersion: v1
kind: ResourceQuota
metadata:
name: runwhen-platform-quota
spec:
hard:
requests.ephemeral-storage: "50Gi"
limits.ephemeral-storage: "100Gi"
requests.storage: "200Gi"
persistentvolumeclaims: 20
---
apiVersion: v1
kind: LimitRange
metadata:
name: runwhen-platform-limits
spec:
limits:
- type: Container
default:
ephemeral-storage: "2Gi"
defaultRequest:
ephemeral-storage: "256Mi"
max:
ephemeral-storage: "10Gi"

On OpenShift, bake these into your ProjectRequestTemplate so every new namespace gets them automatically:

apiVersion: template.openshift.io/v1
kind: Template
metadata:
name: project-request
objects:
- 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"
- 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"

See the charts test file for a validation pod that demonstrates the enforcement difference.


DNS and TLS

DNS

You provide a DNS zone (or delegated subdomain) for global.domain. Typical records created at install include:

HostnameComponent
app.<domain>Web UI
papi.<domain>Platform API
agentfarm.<domain>AI orchestration
runner-control.<domain>Runner mTLS endpoint
vault.<domain>Secret store (operator access)
s3.<domain> (optional)Object store endpoint
mcp.<domain> (optional)MCP server for IDE / AI clients

Records can be managed manually or with external-dns if you grant it API access to your DNS provider.

TLS modes

ModeAir-gap compatible?When to use
Chart-managed CA (runwhenCA.deploy: true)YesSelf-signed wildcard; distribute the CA to browsers / runners
Your ClusterIssuer (internal PKI)YesMost common enterprise mode — bring a corporate CA-backed issuer
Existing wildcard SecretYesPre-provisioned TLS Secret; no cert-manager involvement for ingress (still required for runner mTLS)
Public ACME (Let’s Encrypt)NoRequires reachability to ACME servers; not viable for air-gap

If outbound HTTPS passes through a TLS-inspecting proxy, configure global.trustBundle with your corporate CA — platform pods validate JWTs, OIDC discovery, and webhook destinations over HTTPS.


LLM and embedding requirements

Core AI features (issue analysis, summaries, semantic search, agentic chat) require models you provide. RunWhen ships the in-cluster LiteLLM gateway and Vault-backed credential storage; you point them at your endpoints.

CapabilityWhat you supplyNotes
Chat / completionEndpoint + API keyAzure OpenAI, OpenAI, Bedrock, vLLM, Vertex — anything LiteLLM speaks. See BYO LLM and Self-Hosted LLM Configuration.
EmbeddingsOpenAI-compatible POST /v1/embeddings endpoint with fixed dimension (default 1536)Changing dimension requires re-indexing the search graph — pin once, then leave alone.

PII scrubbing happens before prompts leave the cluster. Hosted OpenAI defaults in sample values are for evaluation only; production self-hosted points at internal endpoints.


Security and compliance

AreaDefaultConfigurable
Pod hardeningPod Security Standard restricted (non-root, read-only rootfs, dropped capabilities, seccompProfile: RuntimeDefault)Yes — relax via global.podSecurityContext / global.containerSecurityContext. On OpenShift/OKD, bundled Postgres needs a documented SCC exception or external database — see OpenShift / OKD guide
SecretsHashiCorp Vault, auto-unsealed via the in-cluster unsealerBring your own KMS for production unseal; capture root token at install
Service accountsChart creates themSet serviceAccount.*.create: false and pre-provision for namespace-scoped admins (BYO-SA)
Trust bundleEmptyglobal.trustBundle projects your corporate CA into every first-party pod
Data residencyAll workspace data in your clusterLLM traffic only to endpoints you configure
ObservabilityCustomer-managedAdd chart overlays for metrics / logs scraping
Resource quotasNot set by the chartRecommended — apply ResourceQuota + LimitRange to the install namespace. See the Resource quotas prerequisite and the Storage requirements section for copy-pasteable YAML. On OpenShift, bake into ProjectRequestTemplate.

Network summary

Path A — connected cluster

Outbound HTTPS from cluster nodes and install workstation to:

  • RunWhen registry (*.docker.pkg.dev) authenticated with your issued credential
  • ghcr.io, registry-1.docker.io, and the five Helm chart hosts listed under Enterprise registry requirements
  • Your LLM endpoints (if external)

Path B / C — mirrored or air-gapped

  • Cluster nodes: pull only from your enterprise registry — no public internet required
  • Mirror / install workstation: scheduled or one-time sync from upstreams; RunWhen credential lives only here
  • Air-gap transfer: image manifest + chart tarball cross the gap on approved media

RunWhen Local runner clusters always need outbound HTTPS to the platform endpoints (papi.<domain>, runner-control.<domain>, vault.<domain>).


Installation workflow (high level)

  1. RunWhen entitles you and ships registry credentials + onboarding pack.
  2. Stand up (or extend) your enterprise registry — see the JFrog guide or the equivalent for Harbor / ECR / Nexus.
  3. Verify cluster prerequisites: cert-manager, ingress, StorageClass, DNS.
  4. Register Helm subchart aliases on the install workstation.
  5. helm pull oci://<registry>/.../charts/runwhen-platform --version <X.Y.Z> --untar
  6. helm dependency update ./runwhen-platform
  7. Apply your values overlay (domain, TLS, registry routing, secrets, LLM bootstrap).
  8. Create the namespace pull secret and helm upgrade --install.
  9. Run the bundled install-checklist.sh verify script.
  10. Deploy RunWhen Local runner(s) pointed at your platform URL.

Detailed commands, placeholder tables, and troubleshooting live in the onboarding pack so they stay in lockstep with your pinned chart version.


TopicLink
Enterprise SSO (OIDC)Single Sign On — Self-hosted
JFrog / Artifactory registry setupJFrog Registry Setup guide
Google Artifact Registry setupGAR Registry Setup guide
Private runners (task execution)SaaS with Private Runner — same chart, self-hosted platform URL
LLM configurationBring Your Own LLM · Self-Hosted LLM Configuration
Slack integration (optional)Slack Integration — Self-Hosted
Azure OpenAI worked exampleAzure OpenAI BYO LLM Setup
Deployment model comparisonDeployment Options
Storage RequirementsStorage Requirements guide — PVC sizing, emptyDir scratch volumes, NFS guidance, resource quotas
OpenShift / OKD installOpenShift & OKD — Self-Hosted
External PostgreSQLExternal PostgreSQL requirements
External S3External S3 requirements
Security modelSecurity Model Overview

Get started

Email support@runwhen.com with:

  • Target environment (cloud provider, air-gap yes/no, preferred enterprise registry)
  • Expected cluster count (platform + runners)
  • Compliance drivers (FedRAMP, FISMA, data residency, etc.)
  • Timeline and chart-version requirements

RunWhen schedules a technical onboarding session, issues registry credentials, and delivers the install pack matched to your path (direct, mirror, or air-gap).