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 alsairy/faheem-code-app repository. Clone it and install from the local path:

git clone https://github.com/alsairy/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/alsairy/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 Pendingno 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).