Skip to content

Helm Values Reference

This page is the reference for the values the TruePPM Helm chart (packages/helm/values.yaml) exposes: what each knob does and the value it ships with. For how many of each resource to run at a given team size, see Deployment Sizing; for the application environment variables passed under env, see Configuration.

KeyDefaultWhat it does
replicaCount1API tier replica count. Raise to 2+ for production (the prod overlay sets 2). Request throughput scales with this because uvicorn runs one worker per pod by default.
image.repositoryregistry.gitlab.com/trueppm/trueppm/apiAPI container image.
image.webRepositoryregistry.gitlab.com/trueppm/trueppm/webWeb (nginx SPA) image; shares tag/pullPolicy with the API so a release deploys a matching pair.
image.tag""Empty pins the chart to its own appVersion for reproducible rollbacks, resolving to v<appVersion> (e.g. v0.4.0) — released images are published under v-prefixed tags, so the v is part of the tag, not decoration. Override per-deploy with a concrete tag, which is used verbatim.
image.pullPolicyIfNotPresentStandard Kubernetes pull policy.
KeyDefaultWhat it does
service.type / service.portClusterIP / 8000API Service. Stays ClusterIP; the Ingress is the sole external object.
web.enabledtrueServe the compiled React SPA from an in-chart nginx tier. Disable if you front the SPA from your own CDN and want only the API + workers.
web.replicaCount1Web-tier replicas; falls back to replicaCount when unset.
web.containerPort8080Port the unprivileged nginx image listens on (satisfies runAsNonRoot).
web.service.type / web.service.portClusterIP / 80Web Service.
web.maxBodySize50Mnginx client_max_body_size for the web tier. Inert in the default topology — the Ingress sends /api and /ws straight to the API Service, so uploads never traverse this nginx. It binds when you route everything through the web tier instead. See Upload size limits.
KeyDefaultWhat it does
web.adminAccess.enabledtrueRender the /admin/ proxy at all. Set false to return 404 instead — removes the path from the public listener entirely.
web.adminAccess.allowCIDRs[]Source CIDRs permitted to reach /admin/. Empty means deny everything. Matched against nginx’s $remote_addr, which behind an Ingress is the controller’s pod IP, not the operator’s — so this is only meaningful when the web tier sees real client addresses.
web.adminAccess.rateLimit.enabledtrueApply an nginx limit_req zone to the admin login surface.
web.adminAccess.rateLimit.rate5r/mRequests per source IP, matching the Docker Compose deployment.
web.adminAccess.rateLimit.burst2Burst allowance above the sustained rate.
KeyDefaultWhat it does
celeryWorker.concurrency2Prefork pool size, pinned. Never left unset: Celery’s cpu_count() default reads the node’s core count rather than the cgroup CPU limit, so an unpinned worker forks a node-sized pool into a 2Gi pod and OOM-kills the whole background tier. Raise toward the pod’s CPU limit per the sizing profiles.
celeryWorker.maxTasksPerChild100Recycle each prefork child after this many tasks so long-running jobs (workspace export, MS Project import) cannot accumulate RSS for the pod’s lifetime. 0 disables recycling.
celeryWorker.extraArgs[]Extra celery worker flags, appended verbatim and in order — e.g. ["--queues=exports", "--prefetch-multiplier=1"].

Off by default — the ingress class, hostnames, and certificate source are cluster-specific, so a default-on ingress would render a broken object.

KeyDefaultWhat it does
ingress.enabledfalseRender a chart-managed Ingress + edge TLS.
ingress.className""IngressClass to bind (nginx, traefik, …). Empty uses the cluster default.
ingress.annotationsnginx.ingress.kubernetes.io/proxy-body-size: "50m"Controller / cert-manager annotations. The shipped default raises the upload ceiling — see Upload size limits. Helm deep-merges this map, so your own keys are added alongside it.
ingress.hostsone example hostVirtual hosts; each path routes to web or api. List /api and /ws before / so they win longest-prefix matching.
ingress.tls[]TLS Secrets per host. Empty renders HTTP-only — dev/demo only, never production.

Two ceilings sit in front of every import, and they are enforced in different places. Get the order wrong and a valid file is rejected by the proxy before the application ever sees it.

LayerWhereDefault
Ingress controlleringress.annotationsnginx.ingress.kubernetes.io/proxy-body-size50m
Web-tier nginxweb.maxBodySize50M
ApplicationMSPROJECT_MAX_UPLOAD_MB (50), JIRA_IMPORT_MAX_UPLOAD_MB (25), CSV_IMPORT_MAX_UPLOAD_MB (10), SEED_MAX_UPLOAD_MB (5)see Configuration

The rule: keep every transport limit at or above the largest application cap. The application cap is the one that should reject an oversized file, because it returns a validation error naming the limit and the format. A transport limit returns a bare 413 with no explanation and nothing in the logs pointing at the import.

The shipped defaults already satisfy this. If you raise MSPROJECT_MAX_UPLOAD_MB above 50, raise both transport limits to match — otherwise the higher app cap is unreachable.

On other ingress controllers the annotation is a no-op. Traefik uses a buffering middleware with maxRequestBodyBytes; HAProxy uses haproxy.org/client-body-buffer-size. Set the equivalent for your controller.

KeyDefaultWhat it does
postgresql.enabledtrueDeploy the bundled PostgreSQL.
postgresql.auth.username / .databasetrueppm / trueppmBundled DB credentials.
postgresql.auth.password""Empty ⇒ chart generates a strong random password and persists it in the connection Secret (never churned on re-render). Set explicitly only to control the credential.
valkey.enabledtrueDeploy the bundled Valkey. Load-bearing for Channels, the Celery broker, and the cache at once.
valkey.auth.enabledtrueValkey auth on by default.
valkey.auth.password""Same generate-and-persist pattern as PostgreSQL.
global.trueppm.connectionSecretName""Override only if you renamed the chart-owned connection Secret.
KeyDefaultWhat it does
networkPolicy.enabledtrueRestrict datastore ingress to the API/worker pods and default-deny datastore egress. Requires a policy-enforcing CNI (Calico, Cilium, Antrea, …) — silently unenforced without one.
podSecurityContextrunAsNonRoot: true, runAsUser: 1000Pod-level restricted defaults.
containerSecurityContextno-priv-escalation, read-only rootfs, drop ALL caps, RuntimeDefault seccompContainer-level restricted defaults.

Per-tier requests/limits under resources.<tier> for api, worker, beat, and web. Defaults are conservative single-team values (API/worker request 250m / 512Mi, limit 1 / 2Gi; beat and web are light). Each includes an ephemeral-storage request/limit for /tmp scratch (MS Project parse, export, large request buffering). Tune per the sizing profiles.

KeyDefaultWhat it does
probes.api.readinessPath/api/v1/readyzDeep readiness: DB + cache reachable and no unapplied/in-flight migrations, so a rolling upgrade never routes traffic to a pod whose schema and code disagree. Detection of the reverse direction — a database carrying migrations the running image does not ship, i.e. an image rolled back without restoring the schema — ships in 0.4 as migration_state: ahead, gated only for a pod that booted into it so a forward rolling upgrade never pulls the old pods out of the Service. Either way, schema presence is not data compatibility: rolling back across a destructive migration still needs a restore from backup.
probes.api.livenessPath/api/v1/health/Shallow liveness so a transient dependency blip can’t restart-loop the pod.
probes.api.readiness*/liveness*Seconds10/10, 30/30Initial-delay and period tuning.
probes.worker.*ping every 60s, failureThreshold: 3celery inspect ping exec probe — catches a wedged event loop a process-alive check would miss.
probes.beat.*ping every 60s, failureThreshold: 5Beat ping targets broker reachability; generous threshold avoids restarts on a brief worker blip.
KeyDefaultWhat it does
podDisruptionBudget.enabledfalsePDBs for API/worker (maxUnavailable: 1). Only meaningful at replicaCount >= 2; beat is excluded (pinned singleton).
autoscaling.enabledfalseHorizontalPodAutoscaler for the API (and optionally worker). Overrides the static replica count and requires metrics-server. Defaults: API 2–6 replicas at 75% CPU.
logging.level""Fleet-wide DJANGO_LOG_LEVEL (DEBUG/INFO/WARNING/ERROR). Empty keeps the app default.

The env block passes application settings into the API/worker/beat containers. The full catalog lives in Configuration; the knobs operators reach for first:

KeyDefaultWhat it does
env.DJANGO_SETTINGS_MODULEtrueppm_api.settings.prodSettings module.
env.DATABASE_URL / env.REDIS_URLunsetRequired when the bundled datastores are disabled, and rejected while they are enabled (the chart-built URL always wins, so your value would be silently ignored) — the render fails either way, with a message saying which. Two supported shapes, both injected via secretKeyRef so neither reaches a Deployment: a secretKeyRef map naming a Secret you manage (preferred — the credential never passes through Helm), or a URL string (the chart stores it in its own connection Secret, but it persists in your values file / shell history / Helm release Secret on the way). env.REDIS_URL is not required when valkey.sentinel.enabled is true. See Managed datastores.
valkey.sentinel.enabledfalseExperimental (0.4). Use a Valkey Sentinel topology instead of a single endpoint. Only honored when valkey.enabled is false. Validate a real failover in staging before depending on it.
valkey.sentinel.nodes""Comma-separated host:port Sentinel list. Required when valkey.sentinel.enabled is true.
valkey.sentinel.masterName""Name the Sentinels monitor the primary under. Required when valkey.sentinel.enabled is true.
valkey.sentinel.password / .sentinelPassword""Data-node and Sentinel-node passwords. Routed through the chart-owned connection Secret, never rendered into a Deployment.
valkey.sentinel.tlsfalseUse TLS to the Valkey data nodes.
env.TRUEPPM_FRONTEND_BASE_URL""Public origin for absolute deep-links in notification emails.
env.TRUEPPM_THROTTLE_ANON_RATE / _USER_RATE60/min / 1000/minAPI rate limits; probe endpoints are always exempt.
env.TRUEPPM_NUM_PROXIES"1"Trusted reverse-proxy depth for real-client-IP extraction. A wrong value lets clients spoof X-Forwarded-For.
env.TRUEPPM_RATE_LIMIT_ENABLED"true"Global API rate-limiting kill switch. Leave "true" in production. Disabling also requires TRUEPPM_RATE_LIMIT_DISABLE_ACK; for load testing only (details).
env.TRUEPPM_PROJECT_SOFT_DELETE_RETENTION_DAYS"30"Trashed-project hard-delete window.
envFrom[]Bulk-inject env vars from existing Secrets/ConfigMaps (e.g. - secretRef: {name: trueppm-env}) into the API, Celery worker, and the bootstrap/migrate init containers. This is the supported way to supply SECRET_KEY, ALLOWED_HOSTS, and INTEGRATION_ENCRYPTION_KEY — the values prod refuses to boot without — without rendering them in plaintext into env. An explicit env: key of the same name always takes precedence over an envFrom entry.

With postgresql.enabled: false / valkey.enabled: false, the chart can no longer build the connection strings, so you supply them. Both shapes below are injected into every consumer — API, Celery worker, Celery beat, the migrate and bootstrap init containers, and the backup CronJob — via secretKeyRef, so neither renders a credential into a Deployment manifest.

Preferred — a Secret you manage. The URL never passes through Helm, so it is absent from your values file, your shell history, and the Helm release Secret. The chart points the containers straight at your Secret and does not copy the value into its own:

env:
DATABASE_URL:
secretKeyRef:
name: trueppm-db
key: url
REDIS_URL:
secretKeyRef:
name: trueppm-cache
key: url

Alternative — a URL string. The chart moves it into the chart-owned connection Secret and injects it from there, so it stays out of the Deployment; but it passed through Helm, so it persists wherever it was held:

env:
DATABASE_URL: "postgres://user:pass@db.example.com:5432/trueppm?sslmode=require"

An external DATABASE_URL must carry sslmode=requiresettings.prod refuses to boot on a plaintext external database. The chart cannot check this for the secretKeyRef form, since it never sees the value; there the guard is the app’s alone, at boot.

KeyDefaultWhat it does
observability.otlp.endpoint""OTLP collector endpoint. Empty ⇒ telemetry off.
observability.otlp.protocolgrpcgrpc (4317) or http/protobuf (4318).
observability.otlp.serviceNametrueppm-apiResource service.name reported on every exported span/metric.
observability.otlp.enabledtrueMaster export switch (only exports when an endpoint is also set).
observability.otlp.tracesEnabled / metricsEnabledtrue / truePer-signal export toggles, consulted only when enabled is true and an endpoint is set. Turn one off to export only the other.
observability.otlp.tracesSampler / Arg""Trace sampling for busy instances, e.g. parentbased_traceidratio + 0.1. Empty keeps the SDK default (parentbased_always_on).
observability.otlp.headers""Comma-separated key=value OTLP headers (e.g. an auth token), rendered inline. Prefer headersSecret below for anything sensitive.
observability.otlp.headersSecretunsetPrefer this over inline headers so auth tokens never render into a plaintext manifest.
observability.otlp.exportHealth.enabledtrueMaster switch for the live export-health recorder (ADR-0601). When on, each pod records per-signal export success/error/counts into Valkey DB 2 so the System Health → Telemetry card shows a cross-process live strip. false reverts the card to a config-only posture; export itself is unaffected either way. Requires the Valkey DB 2 instance to run maxmemory-policy noeviction — the same requirement the rate-limit counters already impose.
observability.otlp.exportHealth.stalenessSeconds"" (app default 600)How long a pod counts as live after its last export; beyond this a silent pod reads “never” instead of stalled.
observability.otlp.exportHealth.healthyWithinSeconds"" (app default 150)A success newer than this reads healthy; older (but still live) reads stalled (metrics) / idle (traces). Must stay below stalenessSeconds, or the stalled/idle states become unobservable. Set all three exportHealth tuning keys together, or none.
observability.otlp.exportHealth.windowSeconds"" (app default 60)Rolling window the exported-item counts cover; the System Health card labels the strip from it (e.g. “last 60s”).
dashboards.enabledfalseShip the starter Grafana dashboard as a labeled ConfigMap (needs a Grafana sidecar watching for the label below).
dashboards.label / labelValuegrafana_dashboard / "1"Label key/value your Grafana sidecar watches for auto-import. Defaults match the upstream kube-prometheus-stack sidecar convention.
dashboards.annotations{}Extra annotations on the dashboard ConfigMap.
alerts.enabledfalseShip starter PrometheusRule alerts (requires the Prometheus Operator CRDs) covering beat staleness, outbox depth/age, and dead-letter. Thresholds tunable under alerts.thresholds below.
alerts.labels{}Extra labels stamped on the PrometheusRule, e.g. release: kube-prometheus-stack so the operator’s ruleSelector picks it up.
alerts.thresholds.beatStaleFor2mHow long the Beat heartbeat must read stale (via the /api/v1/health/beat/ Blackbox probe) before the alert fires.
alerts.thresholds.outboxDepth500Outbox row-count threshold that starts the outboxDepthFor clock.
alerts.thresholds.outboxDepthFor10mHow long outboxDepth must stay breached before the alert fires.
alerts.thresholds.outboxOldestAgeSeconds900Age (seconds) of the oldest pending outbox row that starts the outboxOldestAgeFor clock.
alerts.thresholds.outboxOldestAgeFor10mHow long outboxOldestAgeSeconds must stay breached before the alert fires.
alerts.thresholds.deadLetter0Dead-letter gauge value that starts the deadLetterFor clock — any dead-lettered message is worth alerting on.
alerts.thresholds.deadLetterFor5mHow long the dead-letter gauge must stay above deadLetter before the alert fires.
otelCollector.enabledfalseDocumentation-only reminder — the chart bundles no Collector; deploy one as a sibling release.
KeyDefaultWhat it does
tests.image.repository / tagcurlimages/curl / 8.11.1Image for the helm test connection-check Job. Only pulled when you run helm test <release>, never during a normal install/upgrade. Runs under the same restricted securityContext as the app containers.
tests.probeReadyztrueWhether the connection check also probes /api/v1/readyz in addition to /api/v1/health/. Set false only when testing this chart against an app image that predates readyz (e.g. a CI drill pinned to the last released image while the chart is ahead of it) — otherwise the probe 404s on an endpoint that image doesn’t have yet.

Off by default — a backup CronJob needs a durable destination, so you turn it on deliberately. This is logical backup only (pg_dump); see Backup & Restore for the full runbook.

KeyDefaultWhat it does
backup.enabledfalseEnable the backup CronJob.
backup.schedule"0 2 * * *"Cron schedule (cluster timezone).
backup.imagepostgres:16-alpineClient-capable image carrying pg_dump/psql (the lean app image has no client binaries).
backup.outputDir/backupsIn-container artifact path (the mounted volume when persistence is on).
backup.mediaDir""Include a local media/attachment PVC in the artifact. Leave empty when attachments live in object storage.
backup.keepDaily / keepWeekly7 / 4keepDaily is enforced in-job; keepWeekly is advisory for an external lifecycle policy.
backup.persistence.*disabled, 10Gi RWOChart-managed PVC destination.
backup.s3.*disabledS3-compatible off-cluster destination; the secret must come from a Kubernetes Secret via existingSecret.
backup.extraVolumes / extraVolumeMounts[]Mount your media PVC read-only when mediaDir is set.
backup.resources100m/256Mi1/512MiBackup job container resources.
KeyDefaultWhat it does
admin.passwordFile/run/trueppm/admin_passwordWhere the one-time bootstrap password is written. Retrieve with kubectl exec <api-pod> -- cat /run/trueppm/admin_password.
admin.email""Bootstrap admin email (defaults to admin@trueppm.com).

Turns a release into a throwaway public demo. A post-install/post-upgrade hook Job seeds the bundled sample project and mints two anonymous, read-only share links — one schedule, one board — which become the only publicly reachable way in. Demo mode also swaps the web tier’s nginx config for an allowlist: /admin/, /ws/ and every /api/ route other than the share projections and the liveness probe return 404, and every response carries X-Robots-Tag: noindex alongside a Disallow: / robots.txt.

ValueDefaultEffect
demo.enabledfalseMaster switch. Everything else in the block is inert while false.
demo.baseUrl""Public origin, no trailing slash. Required when enabled — it cannot be inferred from inside the cluster.
demo.shareToken.schedule""Pinned token for the schedule link. Required when enabled.
demo.shareToken.board""Pinned token for the board link. Required when enabled, and must differ from the schedule token.
demo.backoffLimit2Seed Job retries. Exhaustion fails the release deliberately — a demo without data is broken.
demo.resourcessee values.yamlRequests/limits for the short-lived seed Job.
Terminal window
helm install trueppm ./packages/helm \
-f packages/helm/values-demo.yaml \
--set demo.baseUrl=https://demo.example.com \
--set demo.shareToken.schedule="$(openssl rand -base64 32 | tr -d '=+/')" \
--set demo.shareToken.board="$(openssl rand -base64 32 | tr -d '=+/')"

Two things that are easy to get wrong:

  • Both tokens are required and must differ. Share-link hashes are globally unique, so one token cannot back both links. The chart refuses to render otherwise.
  • Pinning is mandatory, not cosmetic. Because the seed is destructive and share links cascade with their project, an unpinned link would change its public URL on every helm upgrade.

A bootstrap superuser still exists on a demo release — the API creates one on every deploy — but it has no public login surface, because the allowlist closes /admin/. Reach it with kubectl port-forward svc/<release>-trueppm-api 8000:8000.

Ready-made overlay: packages/helm/values-demo.yaml, which also sizes Celery down and disables autoscaling, the PodDisruptionBudget, and backups.