Skip to content

Commit cc4a2a2

Browse files
committed
docs(helm): document null as the way to remove an inherited env key
Setting `app.env.KEY: ""` cannot clear a key that `app.envDefaults` sets: the Secret template drops empty values, and the deployment template treats an empty override as "not overridden" and still inlines the default. Helm's own `KEY: null` deletion is the supported mechanism and already works. The empty-string behavior is load-bearing, not a bug — every key under `app.env` ships as a "" placeholder, and ten collide with a real `envDefaults` value (NEXT_PUBLIC_APP_URL, BETTER_AUTH_URL, ...), so "" has to read as "unspecified" or a default install would blank them out. - README: document `null`, with the --reuse-values and Argo CD valuesObject caveats; correct the claim that `app.env` always wins over `app.envDefaults` - values.yaml + self-hosting docs: same guidance where operators look - sim-helm skill: record why an unset list is the wrong shape here - tests: lock in that null removes a key and "" does not
1 parent 0b4d341 commit cc4a2a2

8 files changed

Lines changed: 199 additions & 3 deletions

File tree

apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,25 @@ Self-hosted deployments (billing disabled) run without plan limits: no rate limi
249249
Neither deployment presets these. The Helm chart previously did, which enforced hosted-plan caps on self-hosted installs; chart 1.5.0 removed the presets so Compose and Kubernetes behave identically.
250250
</Callout>
251251

252+
### Removing a limit you already inherited
253+
254+
If a limit is still enforced after upgrading — most often a `FREE_TABLES_LIMIT` or `FREE_TABLE_ROWS_LIMIT` carried forward from a chart older than 1.5.0, or copied into your own values file — the variable is still reaching the pod. On Helm, remove it by overriding it with `null`:
255+
256+
```yaml
257+
app:
258+
envDefaults:
259+
FREE_TABLES_LIMIT: null
260+
FREE_TABLE_ROWS_LIMIT: null
261+
```
262+
263+
Setting the variable to an empty string does **not** remove it: the chart reads an empty value as "not specified", so the inherited value still applies. Confirm what the pod will actually receive before rolling out:
264+
265+
```bash
266+
helm template <release> sim/sim -f values.yaml | grep -A1 FREE_TABLE # expect no output
267+
```
268+
269+
`null` deletion has no effect under `helm upgrade --reuse-values` — pass your full values with `-f`, or use `--reset-then-reuse-values` (Helm 3.14+). If you deploy with Argo CD, put the `null` in `valueFiles` or the `values` string rather than `valuesObject`, which strips nulls. On Docker Compose, delete the line from your `.env` file.
270+
252271
## Example .env
253272

254273
```bash

helm/sim/.claude/skills/sim-helm/references/values-model.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ The Sim chart splits configuration across **four** layers. Understanding which l
4646

4747
**ESO compatibility.** When `externalSecrets.enabled=true`, the chart-managed Secret is **not rendered** — ESO renders one instead. Anything in Layer 1 must be mapped via `remoteRefs.app.<KEY>` or it's silently missing. Layers 2–4 are unaffected by ESO.
4848

49-
**Override precedence.** Values set in `app.env` (Layer 1 overrides) win over `envDefaults` (Layer 2) — so users who already had operational tunables in `app.env` continue to work.
49+
**Override precedence.** *Non-empty* values set in `app.env` (Layer 1 overrides) win over `envDefaults` (Layer 2) — so users who already had operational tunables in `app.env` continue to work. An *empty* `app.env` value does not override: it reads as "not specified" and the Layer 2 default still applies. To remove a key, override it with `null` (see "I want to REMOVE a key the chart sets" below).
5050

5151
## Where keys live — the canonical list
5252

@@ -103,6 +103,22 @@ app:
103103
104104
Prefer Layer 2 for non-sensitive tunables — keeps the Secret lean and ESO mapping minimal.
105105
106+
### "I want to REMOVE a key the chart sets"
107+
108+
Override it with `null` — Helm's documented deletion mechanism. It drops the key from the merged values, so no template emits it.
109+
110+
```yaml
111+
app:
112+
envDefaults:
113+
FREE_TABLES_LIMIT: null
114+
```
115+
116+
**Do not use `""` — it is a silent no-op — and do not add a chart-level "unset list" to work around that.** Every key under `app.env` in `values.yaml` ships as a `""` placeholder, so the templates must read `""` as "not specified"; ten of those collide with a real `envDefaults` value (`NEXT_PUBLIC_APP_URL`, `BETTER_AUTH_URL`, `NEXT_PUBLIC_BRAND_NAME`, `VERTEX_LOCATION`, `EMAIL_VERIFICATION_ENABLED`, …) and would blank themselves out on every default install if `""` meant "delete". A list-shaped unset key is also the wrong interface — Helm merges dicts but not lists, so it cannot be modified or unset downstream.
117+
118+
The `(ne (toString $value) "<nil>")` guards throughout the templates are what make `null` deletion work — preserve them in any new render path. Regression net: `tests/env-null-deletion_test.yaml`.
119+
120+
Caveat worth passing to operators: `null` has no effect under `helm upgrade --reuse-values`.
121+
106122
### "I want to set my production app URL"
107123

108124
```yaml

helm/sim/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: v2
22
name: sim
33
description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents
44
type: application
5-
version: 1.5.2
5+
version: 1.5.3
66
appVersion: "v0.7.44"
77
kubeVersion: ">=1.25.0-0"
88
home: https://sim.ai

helm/sim/README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,11 +358,40 @@ User-supplied `securityContext` values are merged with the defaults — your val
358358
Other security features:
359359

360360
* `automountServiceAccountToken: false` on the ServiceAccount **and** every pod.
361-
* Every value in `app.env` and `realtime.env` is written to a chart-managed Secret and mounted via `envFrom: secretRef` — no values are inlined on the container spec. This eliminates a sensitivity classifier (no static list of "secret" keys to maintain) and ensures new provider keys can never accidentally leak into pod manifests. Two categories are inlined on the container instead: chart-computed values (`DATABASE_URL`, `SOCKET_SERVER_URL`, `OLLAMA_URL`, `PII_URL`) and operational defaults under `app.envDefaults` / `realtime.envDefaults` (rate limits, timeouts, IVM tunables, feature-flag defaults, branding defaults, `http://localhost:3000` URL fallbacks). Operational defaults are non-sensitive by design — moving them out of `app.env` keeps the Secret small and means External Secrets Operator users only have to map the keys they actually set, not every chart default. A value placed in `app.env` always wins over the same key in `app.envDefaults` (the template skips the inline default when an override exists).
361+
* Every value in `app.env` and `realtime.env` is written to a chart-managed Secret and mounted via `envFrom: secretRef` — no values are inlined on the container spec. This eliminates a sensitivity classifier (no static list of "secret" keys to maintain) and ensures new provider keys can never accidentally leak into pod manifests. Two categories are inlined on the container instead: chart-computed values (`DATABASE_URL`, `SOCKET_SERVER_URL`, `OLLAMA_URL`, `PII_URL`) and operational defaults under `app.envDefaults` / `realtime.envDefaults` (rate limits, timeouts, IVM tunables, feature-flag defaults, branding defaults, `http://localhost:3000` URL fallbacks). Operational defaults are non-sensitive by design — moving them out of `app.env` keeps the Secret small and means External Secrets Operator users only have to map the keys they actually set, not every chart default. A **non-empty** value placed in `app.env` wins over the same key in `app.envDefaults` (the template skips the inline default when an override exists). An **empty** value does not — to remove a key rather than change it, see [Removing an inherited env key](#removing-an-inherited-env-key).
362362
* Optional `networkPolicy.enabled=true` enforces east-west isolation and blocks cloud metadata endpoints in egress.
363363

364364
---
365365

366+
## Removing an inherited env key
367+
368+
To remove a key the chart (or an older values file) sets, override it with `null` — [Helm's documented way](https://helm.sh/docs/chart_template_guide/values_files/) to delete a default key:
369+
370+
```yaml
371+
app:
372+
envDefaults:
373+
FREE_TABLES_LIMIT: null
374+
FREE_TABLE_ROWS_LIMIT: null
375+
```
376+
377+
Or on the CLI: `--set app.envDefaults.FREE_TABLES_LIMIT=null`. This works for `app.env` and `realtime.env` too, and in all three secret modes.
378+
379+
**Setting the key to `""` instead does not remove it.** Every key under `app.env` in `values.yaml` ships as a `""` placeholder, so the templates have to treat an empty string as "the operator said nothing" — if they did not, the ten placeholders that collide with a real `app.envDefaults` value (`NEXT_PUBLIC_APP_URL`, `BETTER_AUTH_URL`, `NEXT_PUBLIC_BRAND_NAME`, `VERTEX_LOCATION`, `EMAIL_VERIFICATION_ENABLED`, …) would blank themselves out on every default install. An empty entry is a silent no-op; `null` is the deletion.
380+
381+
Nulling a key the application cannot start without (`BETTER_AUTH_SECRET`, `ENCRYPTION_KEY`, `INTERNAL_API_SECRET`, or `CRON_SECRET` with `cronjobs.enabled=true`) fails at template time with the existing required-secret error, not at runtime.
382+
383+
> **Caveats.** `null` deletion does not take effect under `helm upgrade --reuse-values` ([helm#30765](https://github.com/helm/helm/issues/30765)) — pass your full values with `-f`, or use `--reset-then-reuse-values` (Helm ≥ 3.14).
384+
>
385+
> On **Argo CD**, put the `null` in `spec.source.helm.valueFiles` or the `values` string. Argo CD strips nulls from the structured `valuesObject` field ([argo-cd#16312](https://github.com/argoproj/argo-cd/issues/16312), [#19781](https://github.com/argoproj/argo-cd/issues/19781)), so a null written there silently does nothing.
386+
387+
The common case is a free-tier cap inherited from a chart release older than the one that stopped presetting them, which shipped `FREE_TABLES_LIMIT: "3"` and `FREE_TABLE_ROWS_LIMIT: "1000"` under `app.envDefaults`. With billing disabled, Sim reads an unset limit as unlimited, so nulling these lifts the cap. Verify before rolling out:
388+
389+
```bash
390+
helm template <release> sim/sim -f values.yaml | grep -A1 FREE_TABLE # expect no output
391+
```
392+
393+
---
394+
366395
## Autoscaling
367396

368397
```yaml
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
suite: removing an inherited env key — null deletion, and why "" does not work
2+
release:
3+
name: t
4+
namespace: sim
5+
defaults: &defaults
6+
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
7+
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
8+
app.env.INTERNAL_API_SECRET: x
9+
app.env.CRON_SECRET: x
10+
postgresql.auth.password: xxxxxxxx
11+
tests:
12+
- it: baseline — a preset envDefaults cap is inlined on the app pod
13+
template: deployment-app.yaml
14+
values:
15+
- values/preset-free-limits.yaml
16+
set:
17+
<<: *defaults
18+
asserts:
19+
- contains:
20+
path: spec.template.spec.containers[0].env
21+
content:
22+
name: FREE_TABLES_LIMIT
23+
value: "3"
24+
- contains:
25+
path: spec.template.spec.containers[0].env
26+
content:
27+
name: FREE_TABLE_ROWS_LIMIT
28+
value: "1000"
29+
30+
- it: an empty string does NOT clear an envDefaults key — it reads as "unspecified"
31+
template: deployment-app.yaml
32+
values:
33+
- values/preset-free-limits.yaml
34+
set:
35+
<<: *defaults
36+
app.env.FREE_TABLES_LIMIT: ""
37+
asserts:
38+
- contains:
39+
path: spec.template.spec.containers[0].env
40+
content:
41+
name: FREE_TABLES_LIMIT
42+
value: "3"
43+
44+
- it: null removes the key from the inlined envDefaults
45+
template: deployment-app.yaml
46+
values:
47+
- values/preset-free-limits.yaml
48+
- values/unset-free-limits.yaml
49+
set:
50+
<<: *defaults
51+
asserts:
52+
- notContains:
53+
path: spec.template.spec.containers[0].env
54+
content:
55+
name: FREE_TABLES_LIMIT
56+
value: "3"
57+
- notContains:
58+
path: spec.template.spec.containers[0].env
59+
content:
60+
name: FREE_TABLE_ROWS_LIMIT
61+
value: "1000"
62+
63+
- it: null removes an app.env key from the chart-managed Secret
64+
template: secrets-app.yaml
65+
values:
66+
- values/preset-free-limits.yaml
67+
- values/unset-free-limits.yaml
68+
set:
69+
<<: *defaults
70+
asserts:
71+
- isNull:
72+
path: stringData.FREE_STORAGE_LIMIT_GB
73+
74+
- it: the app.env key is present in the Secret before it is nulled
75+
template: secrets-app.yaml
76+
values:
77+
- values/preset-free-limits.yaml
78+
set:
79+
<<: *defaults
80+
asserts:
81+
- equal:
82+
path: stringData.FREE_STORAGE_LIMIT_GB
83+
value: "5"
84+
85+
- it: nulling unrelated keys leaves the rest of envDefaults intact
86+
template: deployment-app.yaml
87+
values:
88+
- values/preset-free-limits.yaml
89+
- values/unset-free-limits.yaml
90+
set:
91+
<<: *defaults
92+
asserts:
93+
- contains:
94+
path: spec.template.spec.containers[0].env
95+
content:
96+
name: BETTER_AUTH_URL
97+
value: http://localhost:3000
98+
- contains:
99+
path: spec.template.spec.containers[0].env
100+
content:
101+
name: BILLING_CONCURRENCY_LIMIT_FREE
102+
value: "10"
103+
104+
- it: nulling a boot-critical key is still refused by the existing validator
105+
values:
106+
- values/preset-free-limits.yaml
107+
set:
108+
<<: *defaults
109+
app.env.ENCRYPTION_KEY: null
110+
asserts:
111+
- failedTemplate:
112+
errorMessage: app.env.ENCRYPTION_KEY is required for production deployment
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Simulates a chart release older than the one that stopped presetting free-tier
2+
# caps, plus a values file that carried those keys forward.
3+
app:
4+
envDefaults:
5+
FREE_TABLES_LIMIT: "3"
6+
FREE_TABLE_ROWS_LIMIT: "1000"
7+
env:
8+
FREE_STORAGE_LIMIT_GB: "5"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Helm removes a key from the merged values when it is overridden with null.
2+
app:
3+
envDefaults:
4+
FREE_TABLES_LIMIT: null
5+
FREE_TABLE_ROWS_LIMIT: null
6+
env:
7+
FREE_STORAGE_LIMIT_GB: null

helm/sim/values.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@ app:
321321
# app container — NOT written into the chart-managed Secret and NOT required to be mapped
322322
# when externalSecrets.enabled=true. Override any key by setting `app.envDefaults.KEY` in
323323
# your values file. Move a key into `app.env` above only if it must be treated as secret.
324+
#
325+
# To REMOVE a key rather than change it, override it with `null` — Helm deletes a default
326+
# key when it is overridden with null (e.g. `FREE_TABLES_LIMIT: null`). Setting it to ""
327+
# does NOT remove it: every key under `app.env` ships as a "" placeholder, so the templates
328+
# must read "" as "unspecified". See "Removing an inherited env key" in README.md.
324329
envDefaults:
325330
# Application URLs (override in app.env or app.envDefaults for production)
326331
NEXT_PUBLIC_APP_URL: "http://localhost:3000"

0 commit comments

Comments
 (0)