Skip to content

feat: add Helm chart for Kubernetes deployment#1100

Open
mfournioux wants to merge 26 commits into
getnao:mainfrom
mfournioux:feat/helm-chart
Open

feat: add Helm chart for Kubernetes deployment#1100
mfournioux wants to merge 26 commits into
getnao:mainfrom
mfournioux:feat/helm-chart

Conversation

@mfournioux

@mfournioux mfournioux commented Jul 8, 2026

Copy link
Copy Markdown

Summary

This PR adds a production-ready Helm chart for deploying nao— an open-source analytics agent that transforms natural language into SQL queries — on Kubernetes. This is the template to deploy nao as a managed service on K8s clusters.


What's included

Helm chart structure

helm/
├── Chart.yaml # chart metadata (name, version, dependencies)
├── Chart.lock # locked subchart versions
├── values.yaml # default + customizable configuration
├── README.md # installation guide with examples
├── ARCHITECTURE.md # runtime architecture & resource reference
├── charts/ # subcharts (bitnami/postgresql)
├── templates/ # 10 Kubernetes resource templates
│ ├── _helpers.tpl # reusable template helpers
│ ├── deployment.yaml # main nao workload (container + optional initContainer)
│ ├── service.yaml # ClusterIP service for internal traffic
│ ├── pvc.yaml # persistent volume for local context storage
│ ├── configmap.yaml # nao configuration (contextPath, env defaults)
│ ├── secret.yaml # sensitive values (API keys, auth secret, DB URI)
│ ├── serviceaccount.yaml # service account for the pod
│ ├── hpa.yaml # Horizontal Pod Autoscaler (optional)
│ ├── pdb.yaml # Pod Disruption Budget (optional)
│ ├── NOTES.txt # post-install messages
│ └── tests/
│ └── test-connection.yaml # helm test pod
└── .helmignore


Architecture overview

Runtime topology

                  ┌──────────────┐
                  │   Service    │
                  │ (ClusterIP)  │
                  └──────┬───────┘
                         │
                  ┌──────▼───────┐
                  │   Deployment │
                  │   (nao pod)  │
                  │              │
                  │ ┌──────────┐ │
                  │ │ supervis│ │
                  │ │ -ord     │ │
                  │ │  ┌────┐  │ │
                  │ │  │api │  │ │
                  │ │  │├───┤│  │ │
                  │ │  │ui │  │ │
                  │ │  └────┘  │ │
                  │ └──────────┘ │
                  │              │
                  │ ┌──────────┐ │
                  │ │ git-sync │ │ ← optional initContainer
                  │ │ (clone)  │ │    only when contextSource=local
                  │ └──────────┘ │
                  └──────┬───────┘
                         │
                  ┌──────▼───────┐
                  │ PostgreSQL   │
                  │ (Bitnami)    │
                  └──────────────┘

Nao container components

The nao image runs a supervisord process that manages:

  • FastAPI backend (port 5005) — tRPC API, database migrations, auth
  • Vite frontend (port 3000) — React chat UI with HMR in dev

On startup, supervisord runs as root then drops to uid 1000 (nao user) for security.

Three context modes

Nao supports three ways to load project context (database schemas, metadata, docs):

Mode Value How it works K8s resources
Local local Context stored in PVC; optional git-sync initContainer clones from Git PVC, optional initContainer
Git git Entry point clones from NAO_CONTEXT_GIT_URL on startup (no initContainer needed) None extra
API api Dynamic deployment via nao deploy CLI — no context volume at all None extra

The git-sync initContainer is only created when all three conditions are met:

  1. contextSource is local
  2. persistence.enabled is true
  3. gitSync.url is provided (non-empty)

It uses the official registry.k8s.io/git-sync image and injects a read token directly into the HTTPS URL (no SSH, no custom CA certs).

Environment variables

The deployment sets the following key environment variables:

Variable Purpose Default
DATABASE_URL PostgreSQL connection string postgresql://<user>:<pass>@<host>:5432/<db>
NAO_DEFAULT_PROJECT_PATH Where context is mounted /app/project
NAO_CONTEXT_SOURCE Context loading mode local
NAO_CONTEXT_GIT_URL Git URL for git mode (empty)
BETTER_AUTH_URL Public auth callback URL (empty, must be set)
OPENAI_API_KEY OpenAI API key for LLM calls (empty, must be set)
BETTER_AUTH_SECRET Session signing secret (empty, generated per deploy)

Deployment procedure

Prerequisites

  • Kubernetes cluster
  • helm
  • kubectl configured
  • A valid OpenAI API key (or compatible LLM provider)

1. Generate secrets

Generate a Better Auth secret (signs sessions) — generate per deployment:

openssl rand -base64 32

Save the output — you'll pass it to helm install. You'll also need:

  • Your OpenAI API key
  • Your PostgreSQL admin password (or let the chart generate one)

2. Basic install (PostgreSQL auto-provisioned)

helm upgrade --install nao ./helm \
  --set config.betterAuthSecret=<your-secret> \
  --set config.openaiApiKey=<your-key> \
  --set postgresql.auth.password=<db-password>

This deploys everything: nao pod, PostgreSQL (Bitnami), service.

3. Custom install with git-sync

If you want to sync project context from Git on startup:

helm upgrade --install nao ./helm \
  --set config.betterAuthSecret=<your-secret> \
  --set config.openaiApiKey=<your-key> \
  --set persistence.enabled=true \
  --set gitSync.url=https://github.com/your-org/your-context.git \
  --set gitSync.branch=main \
  --set gitSync.token=<read-token>

The initContainer clones the context repo into the PVC before nao starts.

4. Using your own PostgreSQL

helm upgrade --install nao ./helm \
  --set config.betterAuthSecret=<your-secret> \
  --set config.openaiApiKey=<your-key> \
  --set postgresql.enabled=false \
  --set config.databaseUrl=postgresql://user:pass@external-db:5432/nao

5. Verify deployment

# Check pods
kubectl get pods -l app.kubernetes.io/name=nao

# Check logs
kubectl logs -l app.kubernetes.io/name=nao

# Port-forward for local testing
kubectl port-forward svc/nao 5005:80
# → open http://localhost:5005

Values reference

See helm/values.yaml for the complete list of configurable values. Key sections:

Section Purpose
image Container image (repo, tag, pullPolicy)
replicaCount Number of nao replicas
config Nao app settings (auth URL, API keys, context path)
postgresql Subchart config (or disable for external DB)
persistence PVC settings for local context storage
gitSync InitContainer config (URL, branch, token)
secrets API keys (OpenAI, Anthropic), auth secret, DB URI
service Service type, port, annotations
resources CPU/memory limits for the pod
autoscaling HPA config (min/max replicas, target CPU/memory)
podDisruptionBudget PDB config
nodeSelector, tolerations, affinity Scheduling constraints

Design decisions

  • No hardcoded secrets: All secrets default to "" with inline comments instructing users to generate them per deployment
  • Bitnami PostgreSQL subchart: For zero-config deployments; disable and provide external DB for production
  • Image pull: Defaults to IfNotPresent — use imagePullSecrets for private registries

Testing

The chart has been validated with:

helm lint ./helm                          # linting passes
helm template ./helm > /dev/null          # renders 10 resources
helm template ./helm --set contextSource=git | grep -c "kind:"  # git mode: 7 resources
helm template ./helm --set contextSource=local --set persistence.enabled=true --set gitSync.url=https://... | grep -c "kind:"  # local+git: 9 resources
helm template ./helm --set contextSource=api | grep -c "kind:"  # api mode: 6 resources

Review in cubic

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

This PR was auto-closed. Only contributors approved with lgtm can open PRs. Open an issue first.

Maintainers review auto-closed issues daily. Issues that do not meet the quality bar in CONTRIBUTING.md will not be reopened or receive a reply.

If a maintainer replies lgtmi, your future issues will stay open. If a maintainer replies lgtm, your future issues and PRs will stay open.

See CONTRIBUTING.md.

Removed betterAuthUrl configuration from helm install command.

Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com>
Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com>
@Bl3f Bl3f reopened this Jul 8, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17 issues found across 18 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="helm/templates/deployment.yaml">

<violation number="1" location="helm/templates/deployment.yaml:54">
P2: Context refreshes can leave stale files from previous revisions because `git reset --hard` does not delete untracked files in the persistent checkout. Adding `git clean -fdx` after the reset keeps the mounted project identical to the requested Git revision.</violation>

<violation number="2" location="helm/templates/deployment.yaml:57">
P2: Reinitializing a non-Git PVC can fail when hidden files already exist because `rm -rf .../*` leaves dotfiles behind before cloning into that directory. Removing all children with `find` avoids a "destination path already exists and is not an empty directory" startup failure.</violation>

<violation number="3" location="helm/templates/deployment.yaml:86">
P2: Configuration and secret changes may not reach running pods after `helm upgrade` because the Deployment template does not change when only the ConfigMap or Secret data changes. Adding checksum annotations for those templates to `spec.template.metadata.annotations` would trigger an automatic rollout for updated env values.</violation>
</file>

<file name="helm/README.md">

<violation number="1" location="helm/README.md:117">
P2: Inconsistent `config.betterAuthUrl` default across documentation files. The README values reference lists the default as `"http://localhost:5005"`, while the ARCHITECTURE reference lists it as `""`. Neither matches the actual values.yaml — `config.betterAuthUrl` is not defined at all, so the Helm default is empty string, which produces `BETTER_AUTH_URL=""` in the ConfigMap. Align both docs to the actual default and consider whether the value should be added to values.yaml with a sensible default.</violation>

<violation number="2" location="helm/README.md:127">
P3: Duplicate `secrets.contextGitToken` row in the values reference table — the same entry appears twice consecutively. Remove the duplicate row to keep the table clean and unambiguous.</violation>
</file>

<file name="helm/templates/configmap.yaml">

<violation number="1" location="helm/templates/configmap.yaml:19">
P2: `contextSource=api` renders an environment value the Python context provider rejects, so FastAPI context endpoints report errors instead of representing the API-mode projects volume. The chart should either map API mode to supported runtime env vars or add runtime support before advertising/rendering `NAO_CONTEXT_SOURCE=api`.</violation>
</file>

<file name="helm/values.yaml">

<violation number="1" location="helm/values.yaml:29">
P2: Production installs default to an archived PostgreSQL image with Helm image verification disabled, so users inherit an unpatched database image unless they override values. It would be safer to use a supported image path by default or make `bitnamilegacy`/`allowInsecureImages` an explicit opt-in migration setting.</violation>

<violation number="2" location="helm/values.yaml:85">
P1: Default Helm installs can start with an invalid `BETTER_AUTH_URL`, causing backend env validation to fail before the service is usable. `values.yaml` should define the same URL default that the template and README expect, e.g. `http://localhost:5005`.</violation>
</file>

<file name="helm/ARCHITECTURE.md">

<violation number="1" location="helm/ARCHITECTURE.md:1">
P3: The Application config (ConfigMap) reference table is missing 6 LLM provider base URL values that exist in `values.yaml` and are rendered into the ConfigMap template: `config.openaiBaseUrl` → `OPENAI_BASE_URL`, `config.anthropicBaseUrl` → `ANTHROPIC_BASE_URL`, `config.geminiBaseUrl` → `GEMINI_BASE_URL`, `config.mistralBaseUrl` → `MISTRAL_BASE_URL`, `config.openrouterBaseUrl` → `OPENROUTER_BASE_URL`, `config.ollamaBaseUrl` → `OLLAMA_BASE_URL`. Also missing `config.gitSync.image`. Add them to keep the reference complete.</violation>

<violation number="2" location="helm/ARCHITECTURE.md:227">
P3: The Secret vs ConfigMap split section lists `NAO_LICENSE` as a key rendered from secrets, but no `NAO_LICENSE` key exists in the secret template (`helm/templates/secret.yaml`) or in `helm/values.yaml`. This looks like a leftover from a now-removed feature. Remove it from the doc to avoid confusion.</violation>
</file>

<file name="helm/templates/_helpers.tpl">

<violation number="1" location="helm/templates/_helpers.tpl:96">
P1: The `DB_URI` secret will contain a broken connection string when `postgresql.enabled=true` and `postgresql.auth.password` is not explicitly set. The default password is an empty string in `values.yaml`, and at Helm template-render time `.Values.postgresql.auth.password` is still `""` — so the rendered `DB_URI` becomes `postgres://nao:@nao-postgresql:5432/nao` with no password. When the application pod starts, it will fail to authenticate against PostgreSQL because the Bitnami subchart generates a random password that is never reflected in this URI.

**Recommendation**: Add a `required` call to fail early with a clear message when the password is empty, instead of silently producing a broken URI at runtime:

```yaml
{{- define "nao.dbUri" -}}
{{- if .Values.postgresql.enabled -}}
{{- $password := required "postgresql.auth.password must be set when postgresql.enabled=true" .Values.postgresql.auth.password -}}
{{- printf "postgres://%s:%s@%s:5432/%s"
    .Values.postgresql.auth.username
    $password
    (include "nao.postgresqlHost" .)
    .Values.postgresql.auth.database -}}
{{- else -}}
{{- .Values.secrets.dbUri -}}
{{- end -}}
{{- end }}

Alternatively, update the values.yaml default comment to indicate the password is required (not just "Change in production") and document both fixes.

P2: ServiceAccount template doesn't include `commonAnnotations`, unlike most other resources. The values.yaml comment says commonAnnotations are "already merged automatically" but the template only uses resource-specific `serviceAccount.annotations` — no merge happens. If `commonAnnotations` is needed for Kyverno compliance (mentioned in values.yaml), the ServiceAccount won't pick them up. Either add `commonAnnotations` here (consistent with configmap, secret, deployment, hpa, pdb) or correct the values.yaml comment to avoid misleading users. P1: The default image tag resolves to `:latest` (via `appVersion: "latest"` defaulted into `image.tag`), which is a mutable tag. This means the same chart version can pull a different image after a pod restart or node re-schedule with no chart version change, making rollbacks unreliable and deployments unreproducible. Since this is the official Helm chart for deploying nao as a managed service, a specific default tag (e.g., the latest stable release) would be more appropriate. Consider either pinning `appVersion` to a specific release tag or adding a `defaultTag` entry in values.yaml so the chart version and image tag are independently manageable. ```

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread helm/templates/deployment.yaml Outdated
Comment thread helm/values.yaml
Comment thread helm/templates/_helpers.tpl
Comment thread helm/Chart.yaml
description: Helm chart for nao — an open-source analytics agent that transforms natural language into SQL queries.
type: application
version: 0.1.0
appVersion: "latest"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The default image tag resolves to :latest (via appVersion: "latest" defaulted into image.tag), which is a mutable tag. This means the same chart version can pull a different image after a pod restart or node re-schedule with no chart version change, making rollbacks unreliable and deployments unreproducible. Since this is the official Helm chart for deploying nao as a managed service, a specific default tag (e.g., the latest stable release) would be more appropriate. Consider either pinning appVersion to a specific release tag or adding a defaultTag entry in values.yaml so the chart version and image tag are independently manageable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helm/Chart.yaml, line 6:

<comment>The default image tag resolves to `:latest` (via `appVersion: "latest"` defaulted into `image.tag`), which is a mutable tag. This means the same chart version can pull a different image after a pod restart or node re-schedule with no chart version change, making rollbacks unreliable and deployments unreproducible. Since this is the official Helm chart for deploying nao as a managed service, a specific default tag (e.g., the latest stable release) would be more appropriate. Consider either pinning `appVersion` to a specific release tag or adding a `defaultTag` entry in values.yaml so the chart version and image tag are independently manageable.</comment>

<file context>
@@ -0,0 +1,29 @@
+description: Helm chart for nao — an open-source analytics agent that transforms natural language into SQL queries.
+type: application
+version: 0.1.0
+appVersion: "latest"
+
+keywords:
</file context>

Comment thread helm/templates/serviceaccount.yaml
Comment thread helm/values.yaml Outdated
Comment thread helm/templates/tests/test-connection.yaml Outdated
Comment thread helm/README.md
Comment thread helm/ARCHITECTURE.md
Comment thread helm/ARCHITECTURE.md Outdated
mfournioux and others added 5 commits July 8, 2026 12:07
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="helm/README.md">

<violation number="1" location="helm/README.md:117">
P2: Inconsistent `config.betterAuthUrl` default across documentation files. The README values reference lists the default as `"http://localhost:5005"`, while the ARCHITECTURE reference lists it as `""`. Neither matches the actual values.yaml — `config.betterAuthUrl` is not defined at all, so the Helm default is empty string, which produces `BETTER_AUTH_URL=""` in the ConfigMap. Align both docs to the actual default and consider whether the value should be added to values.yaml with a sensible default.</violation>
</file>

<file name="helm/Chart.yaml">

<violation number="1" location="helm/Chart.yaml:6">
P1: The default image tag resolves to `:latest` (via `appVersion: "latest"` defaulted into `image.tag`), which is a mutable tag. This means the same chart version can pull a different image after a pod restart or node re-schedule with no chart version change, making rollbacks unreliable and deployments unreproducible. Since this is the official Helm chart for deploying nao as a managed service, a specific default tag (e.g., the latest stable release) would be more appropriate. Consider either pinning `appVersion` to a specific release tag or adding a `defaultTag` entry in values.yaml so the chart version and image tag are independently manageable.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread helm/templates/deployment.yaml
mfournioux added 11 commits July 8, 2026 13:33
The Deployment uses envFrom with configMapRef and secretRef but does not
change when only the referenced ConfigMap or Secret data changes, causing
helm upgrade to silently skip rolling out updated env values. Adding
checksum annotations for both templates triggers an automatic rollout
when configuration or secret data changes.
When contextSource=api, the chart set NAO_CONTEXT_SOURCE=api but did not
configure NAO_PROJECTS_DIR, which the backend uses to locate dynamically
deployed projects in /app/projects. This caused context endpoints to fail
in api mode. Adding NAO_PROJECTS_DIR=/app/projects when contextSource=api
ensures the backend can find deployed projects on the PVC
The bitnamilegacy/postgresql image requires allowInsecureImages: true to
pass Bitnami's container image verification. This is a temporary setting
during the migration from the Bitnami postgresql subchart. Document it
clearly in values.yaml so users understand it's a short-term workaround
and know how to remove it when the chart switches to the official
postgres image.
…config table

Add 7 missing values to the Application config (ConfigMap) reference table:
config.gitSync.image, config.openaiBaseUrl, config.anthropicBaseUrl,
config.geminiBaseUrl, config.mistralBaseUrl, config.openrouterBaseUrl,
config.ollamaBaseUrl. These are rendered into the ConfigMap but were
previously undocumented.
NAO_LICENSE was previously removed from templates/secret.yaml and
values.yaml but the Secret vs ConfigMap split section in ARCHITECTURE.md
still references it. Remove it to keep docs consistent.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 8 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread helm/templates/deployment.yaml Outdated
Comment thread helm/values.yaml Outdated
Comment thread helm/templates/service.yaml Outdated
Comment thread helm/templates/_helpers.tpl Outdated
mfournioux and others added 2 commits July 8, 2026 13:54
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com>
Helm Sprig's merge keeps the destination dictionary's keys — the source
only fills in missing keys. The original code used merge
serviceAnnotations, which made commonAnnotations silently override
resource-specific annotations. Changed to merge serviceAnnotations
so that service/serviceAccount-specific annotations take precedence over
commonAnnotations, allowing users to override defaults on individual
resources.
…port

The required guard on postgresql.auth.password breaks valid Bitnami
workflows like auth.existingSecret or global password override. Replace
with a graceful fallback: when existingSecret is set, use secrets.dbUri
from values.yaml instead. Document that existingSecret requires manual
dbUri configuration. This aligns with Bitnami's secret management
patterns while keeping the inline password path working as before.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread helm/templates/deployment.yaml Outdated
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Signed-off-by: Maxime Fournioux <55544262+mfournioux@users.noreply.github.com>
@mfournioux

Copy link
Copy Markdown
Author

Hello @Bl3f, regarding this PR, do you need additional tests to validate the helm chart ?

@Bl3f

Bl3f commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Hey @mfournioux, sorry I was away with limited internet connection the last 2 weeks so it was hard to test the Helm charts. I'll try to work on it before end of week

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants