Skip to main content

Kubernetes (Helm)

The official Helm chart deploys the all-in-one Faheem Code image (frontend + agent-server + automation) on Kubernetes as a StatefulSet with a PersistentVolumeClaim for durable state, a Service, and an optional Ingress and RBAC layer. It's the recommended way to run Faheem Code as a shared, always-on backend that survives pod restarts and image upgrades.

Relationship to Faheem Code Enterprise​

Faheem Code is an unauthenticated, single-tenant application. This Helm chart runs exactly that: one shared instance where all agents are comingled on the same pod and PVC, with no built-in authentication, user-level role-based access control, or tenant isolation. It's a good fit for a single team or individual running their own backend, and for the "internal vibecoding platform" pattern described above — where a small, trusted group shares one deployment.

Faheem Code Enterprise (FCE) is the productized upgrade path when you need a hardened, multi-user deployment. FCE adds:

  • Authentication (SSO / SAML / OIDC) so users must log in before they can run agents.
  • Role-based access control over who can run agents and manage the deployment.
  • Multi-tenancy so different teams get isolated spaces rather than one shared instance.
  • Isolated agent sandboxes — each agent run executes in its own container instead of every agent sharing the pod's filesystem and shell.

Use this Helm chart for self-hosted, single-tenant setups; reach for FCE when you need authentication, multi-tenancy, or isolated agent execution.

Prerequisites​

  • Kubernetes 1.24 or later (required by the chart's kubeVersion constraint).
  • Helm 3.x.
  • A working kubectl context with permission to create resources in the target namespace.
  • A StorageClass that supports ReadWriteOnce volumes. On GKE this is usually standard-rwo on older node pools or hyperdisk-balanced on c4 / n4 node pools. On EKS it's gp3. On DigitalOcean/Linode it's do-block-storage / linode-block-storage.
  • An ingress controller (nginx, Traefik, cloud-provider ingress, etc.) if you want to reach Faheem Code from outside the cluster.

Get the chart​

The chart lives alongside the source in the SMART-National-Solution/faheem-code-app repository. Clone it and install from the local path:

git clone https://github.com/SMART-National-Solution/faheem-code-app.git
cd Faheem Code
helm install faheem-code ./helm/faheem-code \
--namespace faheem-code --create-namespace

That single command deploys everything below. Faheem Code is now reachable inside the cluster at http://faheem-code.faheem-code.svc.cluster.local:8000. See Access It for how to reach it from a browser.

What gets deployed​

ResourcePurpose
StatefulSetSingle-replica pod running the all-in-one image.
PersistentVolumeClaim (per pod)Backs ~/.faheem-code and ~/workspace (both mounted from the same PVC via subPath): settings, encrypted secrets, conversation history, automation SQLite DB, cloned repos, generated files.
Service (ClusterIP)Cluster-internal endpoint on port 8000.
Service (headless)Required by the StatefulSet for stable pod DNS.
ServiceAccountStable identity the pod runs under.
Ingress (optional)External HTTP(S) entry point.
RoleBinding (per namespace)Created when rbac.enabled=true, one per entry in rbac.namespaces.
ClusterRoleBinding (optional)Created when rbac.clusterAdmin=true.

Persistence​

The chart provisions one PVC and mounts it at multiple well-known subdirectories of the faheemcode user's HOME via subPath. That preserves the pristine /home/faheemcode the base image ships (dotfiles like ~/.bashrc and ~/.profile) while persisting the directories that actually contain state:

  • ~/.faheem-code — agent-server settings and encrypted secrets, conversation history and event stores, automation SQLite database (unless you point at external Postgres — see External Database), the FC_SECRET_KEY and session API key auto-generated on first boot
  • ~/workspace — the agent's default working directory: cloned repos, worktrees, anything the agent writes when it treats ~ as the workspace root

Both paths share the same underlying disk. Add more entries to persistence.mounts if you want other subtrees persisted (e.g. ~/.cache, ~/.config).

Defaults:

persistence:
enabled: true
mounts:
- mountPath: /home/faheemcode/.faheem-code
subPath: faheemcode
- mountPath: /home/faheemcode/workspace
subPath: workspace
size: 20Gi
# storageClassName: "" # empty → cluster default
accessModes:
- ReadWriteOnce

Bring your own PVC​

If you already manage the volume out of band, point the chart at it and it will skip the volumeClaimTemplates path:

persistence:
enabled: true
existingClaim: my-faheem-code-pvc

Ingress​

Ingress is off by default. Enable it and provide the standard knobs — the chart supports className, annotations, multiple hosts with per-path routing, and TLS.

ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: faheem-code.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- faheem-code.example.com
secretName: faheem-code-tls

RBAC​

RBAC is off by default. The pod runs under its own ServiceAccount but has no in-cluster permissions. Turn it on when the agent needs to inspect or mutate Kubernetes resources (e.g. to deploy things it builds).

Two independent switches:

rbac:
enabled: true
# Full access to all resources in these namespaces (bound to the
# built-in `admin` ClusterRole via one RoleBinding per namespace).
# Each namespace must already exist in the cluster.
namespaces:
- default
- agent-sandbox
# Optionally grant cluster-admin. OFF by default. Very broad — enable
# only when the agent truly needs to manage the whole cluster.
clusterAdmin: false

Skill: deploying apps into the cluster​

To unlock the internal vibecoding platform described at the top of this page, give the agent a skill that teaches it the conventions for shipping the apps it builds into a namespace of your cluster. Drop the markdown below into .faheem-code/skills/deploy-app/SKILL.md (or your workspace's skills directory), adjust the placeholders (<namespace>, <domain>, GitHub org), and the agent will scaffold, deploy, and — with the GitHub MCP server enabled — push each app to its own repo on request.

This is a generic version of the skill the Faheem Code team runs internally. It assumes the backend was installed with rbac.enabled=true and a rbac.namespaces entry for the target namespace, so the pod's ServiceAccount can kubectl apply there directly.

# Deploy apps into the cluster

Use this skill to create and manage the small web apps you build, serving each
one at `https://<name>.<domain>` from the `<namespace>` namespace of the
cluster Faheem Code runs in.

## Platform conventions

Every app follows the same pattern:

- **Namespace:** `<namespace>`. The agent runs under a ServiceAccount that has
admin in this namespace (granted via the Helm chart's `rbac.namespaces`), so
`kubectl apply` works directly with no extra credentials.
- **Content:** static files (HTML/JS/CSS) served by an `nginx:*-alpine` pod.
The files live in a **ConfigMap** (`<name>-web`) mounted at
`/usr/share/nginx/html`. Apps that need a backend add their own container.
- **Objects per app:** `Deployment` + `Service` (ClusterIP, port 80) +
`Ingress`. Apps that need scheduled work add a `CronJob`.
- **Host:** `<name>.<domain>`.
- **TLS:** if cert-manager is installed, add the
`cert-manager.io/cluster-issuer: <issuer>` annotation and a `tls` block with
`secretName: <name>-tls`; the cert is issued automatically.
- **Auth:** put shared apps behind your ingress's authentication (oauth2-proxy,
a forward-auth middleware, Cloudflare Access, etc.) so they aren't exposed
unauthenticated. Reference your cluster's auth middleware/annotation here.
- **Resources:** keep them tiny (requests `10m`/`16Mi`, limits `100m`/`64Mi`).

### Ingress template

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: <name>
namespace: <namespace>
labels:
app: <name>
annotations:
cert-manager.io/cluster-issuer: <issuer>
# Add your cluster's auth middleware/annotation here so the app is
# not exposed unauthenticated.
spec:
ingressClassName: <ingress-class> # e.g. nginx or traefik
rules:
- host: <name>.<domain>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: <name>
port:
number: 80
tls:
- hosts:
- <name>.<domain>
secretName: <name>-tls
```

### Deployment + Service template

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: <name>
namespace: <namespace>
labels:
app: <name>
spec:
replicas: 1
selector:
matchLabels:
app: <name>
template:
metadata:
labels:
app: <name>
spec:
containers:
- name: web
image: nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: web
mountPath: /usr/share/nginx/html
volumes:
- name: web
configMap:
name: <name>-web
---
apiVersion: v1
kind: Service
metadata:
name: <name>
namespace: <namespace>
labels:
app: <name>
spec:
selector:
app: <name>
ports:
- port: 80
targetPort: 80
```

## Secrets (never commit them)

If an app needs credentials at runtime, store them in a Kubernetes `Secret`
created out of band — never in a ConfigMap, the git repo, or the manifest, and
never print their values. Create the Secret from environment variables so the
plaintext never appears in a command line or file:

```bash
kubectl create secret generic <name>-<purpose> -n <namespace> \
--from-literal=<key>="$SOME_ENV_VAR" \
--dry-run=client -o yaml | kubectl apply -f -
```

Consume it in the Deployment via `env` + `secretKeyRef`. Document the one-off
`kubectl create secret ...` command in the app's `README.md` — keep it out of
`deploy.sh` and the committed manifest.

## Source layout & GitHub

- Keep each app's source under `~/workspace/<project>`.
- Give each app its own GitHub repo (e.g. `<github-org>/app-<project>`).
**This requires the GitHub MCP server to be enabled** so the agent can create
the repo and push commits. Create it if it doesn't exist, then push.
- Standard repo layout:
- `README.md` — what the app is, its URL, how to deploy, any one-off secrets.
- `k8s/<project>.yaml` — all Kubernetes objects (Deployment+Service+Ingress).
- `web/` — static assets served via the ConfigMap.
- `deploy.sh` — regenerates the ConfigMap from `web/`, applies `k8s/`, and
rolls the Deployment.

### Typical `deploy.sh`

```bash
#!/usr/bin/env bash
set -euo pipefail
NS=<namespace>
DIR="$(cd "$(dirname "$0")" && pwd)"

kubectl create configmap <name>-web --namespace "$NS" \
--from-file="$DIR/web/" \
--dry-run=client -o yaml | kubectl apply -f -

kubectl apply -f "$DIR/k8s/<name>.yaml"
kubectl rollout restart deployment/<name> -n "$NS"
kubectl rollout status deployment/<name> -n "$NS"
```

## Creating a new app

1. `mkdir -p ~/workspace/<project>/{k8s,web}` and add `web/index.html`,
`k8s/<project>.yaml` (from the templates above), `deploy.sh`, and a
`README.md`.
2. If GitHub MCP is enabled, create/verify `<github-org>/app-<project>`,
commit, and push.
3. Deploy: `./deploy.sh`.
4. If cert-manager is used, wait for the cert:
`kubectl get certificate <name>-tls -n <namespace>` should become
`READY=True`. Confirm the Ingress has an address.
5. Report the live URL back to the user.

## Updating an app

Edit files under `~/workspace/<project>`, commit + push (via GitHub MCP), then
re-run `./deploy.sh` (which restarts the Deployment so nginx reloads the
ConfigMap).

## Deleting an app

1. `kubectl delete -f ~/workspace/<project>/k8s/<project>.yaml` and delete the
`<name>-web` ConfigMap. Deleting the Ingress lets cert-manager clean up the
TLS secret; delete any credential secrets explicitly.
2. Optionally archive/delete the GitHub repo and remove
`~/workspace/<project>`.

## Verifying access

```bash
kubectl get deploy,svc,ingress,certificate -n <namespace> -l app=<name>
```

Common configurations​

Minimal (defaults + ingress)​

# values.yaml
ingress:
enabled: true
className: nginx
hosts:
- host: faheem-code.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts: [faheem-code.example.com]
secretName: faheem-code-tls

With LLM credentials from a secret​

Rather than typing your LLM key into the UI on every reinstall, pass it in through the chart. Create the secret separately, then reference it via config.extraEnv:

kubectl -n faheem-code create secret generic llm \
--from-literal=api-key=sk-...
# values.yaml
config:
extraEnv:
- name: LLM_MODEL
value: "faheemcode/claude-sonnet-4-5-20250929"
- name: LLM_API_KEY
valueFrom:
secretKeyRef:
name: llm
key: api-key

Agent that manages a sandbox namespace​

# values.yaml
rbac:
enabled: true
namespaces:
- agent-sandbox

Create the sandbox namespace before installing (kubectl create namespace agent-sandbox). Then the pod can kubectl apply / kubectl delete anything inside agent-sandbox but nothing else.

External database​

The automation subsystem uses a SQLite database on the PVC by default. For higher-volume deployments, point it at Postgres:

# values.yaml
config:
automationDbUrl: "postgresql+asyncpg://user:pass@postgres.databases.svc.cluster.local/faheem_code"

Store the actual credentials in a Kubernetes Secret and reference them via config.extraEnv rather than putting the password in values.yaml.

Install and upgrade​

# First install
helm install faheem-code ./helm/faheem-code \
--namespace faheem-code --create-namespace \
-f values.yaml

# Later upgrades
helm upgrade faheem-code ./helm/faheem-code \
-n faheem-code -f values.yaml

# Check rollout
kubectl -n faheem-code rollout status statefulset/faheem-code
kubectl -n faheem-code get pvc,pod,svc,ingress

To pin a specific image (e.g. a PR preview or a build newer than the chart's appVersion):

helm upgrade faheem-code ./helm/faheem-code \
-n faheem-code -f values.yaml \
--set image.tag=sha-<git-sha>

Access it​

The chart's default Service is ClusterIP. Three common ways to reach the UI:

  1. Ingress — configure the ingress: block as shown above. This is the production path.

  2. Port-forward — for quick access from your laptop without touching DNS or ingress:

    kubectl -n faheem-code port-forward svc/faheem-code 8000:8000

    Then open http://localhost:8000/canvas.

  3. LoadBalancer — set service.type: LoadBalancer if your cloud provisions cloud load balancers for you. Cheaper than ingress for one-off installs, but skips TLS and auth.

Uninstall​

helm uninstall faheem-code -n faheem-code

The PVC created by the StatefulSet is retained on uninstall so a reinstall picks up where you left off. Delete it explicitly if you want a fully clean slate:

kubectl -n faheem-code delete pvc -l app.kubernetes.io/instance=faheem-code

Troubleshooting​

FailedAttachVolume: pd-balanced disk type cannot be used by c4-standard-8 machine type​

The default StorageClass on your cluster is provisioning a disk type your nodes can't attach. On GKE c4 / n4 node pools, use hyperdisk-balanced:

persistence:
storageClassName: hyperdisk-balanced

Because volumeClaimTemplates on an existing StatefulSet are immutable, changing the StorageClass requires deleting the STS and PVC first:

kubectl -n faheem-code delete statefulset faheem-code
kubectl -n faheem-code delete pvc -l app.kubernetes.io/instance=faheem-code
helm upgrade faheem-code ./helm/faheem-code -n faheem-code -f values.yaml

ErrImagePull on ghcr.io/smart-national-solution/faheem-code-app:<tag>​

Verify the tag exists on GHCR — the chart's appVersion pins the default. To pull an image built from a specific commit, use --set image.tag=sha-<short-sha>. See the Faheem Code package for the tag list.

WebSocket disconnects every minute​

Your ingress is closing idle streams. Bump the timeout annotations on the Ingress:

ingress:
annotations:
# nginx
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# Traefik
traefik.ingress.kubernetes.io/router.middlewares: "" # keep this in mind if you also add auth middlewares

Pod stuck in Pending — no persistent volumes available​

Either no StorageClass exists on the cluster, or the one you set doesn't provision on demand. Run kubectl get storageclass and set persistence.storageClassName to one that shows VOLUMEBINDINGMODE=WaitForFirstConsumer (Immediate is fine too).