RagLeap Packages
Looking for the self-hosted AI employee system? This page documents the open-source pip install ragleap-ops package. To install and run ragleap-core (AI employees, channels, Docker), see docs.ragleap.com; the project home is ragleap.com.
Live-tested end-to-end on a real kind cluster — not just schema-validated

ragleap-ops

Kubernetes Deployment and Service manifests, plus a Helm chart wrapping them, for RagLeap Core — translated field-by-field from the real docker-compose.yml and proven against a live cluster.

0.5.0
PyPI version
4 / 4
Services reached 1/1 Running
1
Real bug found & fixed live

Install

The Python package itself ships no functional code — it exists so this content can be versioned and released through the same PyPI pipeline as the other RagLeap packages. The real deliverable is the k8s/ directory.

pip install ragleap-ops
# or, with uv
uv add ragleap-ops

What's inside

Deployment + Service manifests for all four docker-compose.yml services, plus the Secrets/PVCs/ConfigMap they depend on.

db-deployment.yaml
pgvector/pgvector:pg16. PVC-backed, schema loaded via a ConfigMap generated from the real db/schema.sql.
app-deployment.yaml
Pulls ghcr.io/antonyrag/ragleap-app:latest, built from the repo-root Dockerfile. Waits on db via an init container polling pg_isready.
voice-deployment.yaml
Same image as app, command overridden to python3 -m channels.voice.server, listens on 8765.
neo4j-deployment.yaml
neo4j:5-community, ports 7474/7687, PVC-backed, readiness via HTTP check against 7474.

Apply order

Dependency order matters — app/voice won't start cleanly without db already healthy.

kubectl apply -f db-secret.yaml -f neo4j-secret.yaml -f db-schema-configmap.yaml
kubectl apply -f db-pvc.yaml -f neo4j-pvc.yaml
kubectl apply -f db-deployment.yaml -f db-service.yaml
kubectl rollout status deployment/ragleap-db
kubectl apply -f app-deployment.yaml -f voice-deployment.yaml -f neo4j-deployment.yaml
kubectl apply -f app-service.yaml -f voice-service.yaml -f neo4j-service.yaml

Two files are intentionally not shipped in the package (see .gitignore) since they'd otherwise bake real secret values into git history — generate them locally:

kubectl create secret generic ragleap-app-env \
  --from-env-file=.env --dry-run=client -o yaml > app-env-secret.yaml

Secrets management for production (Sealed Secrets)

Local testing generates secrets via kubectl create secret --from-env-file, but that leaves real credentials in plaintext files and shell history. Production uses Sealed Secrets instead — secrets are encrypted against your specific cluster's public key, so the encrypted result is safe to commit to git.

kubeseal --controller-name sealed-secrets-controller \
  --controller-namespace kube-system --fetch-cert > sealed-secrets-pub-cert.pem

kubectl create secret generic ragleap-db-secret \
  --dry-run=client \
  --from-literal=POSTGRES_USER=ragleap \
  --from-literal=POSTGRES_PASSWORD=your-real-password \
  -o yaml | kubeseal --format yaml --cert sealed-secrets-pub-cert.pem > db-secret-sealed.yaml

A sealed secret is tied to the exact cluster whose public key encrypted it — never copy a sealed file between environments; reseal against each target cluster's own cert.

Helm chart hardening

The Helm chart (helm/ragleap-ops/) is the install path most users should take. Since 0.4.0 it also carries the least-privilege settings that previously existed only in the raw k8s/ manifests.

ServiceAccounts
One zero-permission ServiceAccount each for db, app, voice and neo4j (serviceAccount.create, default true). No Role or RoleBinding is created and no API token is mounted. Verified live on the db pod: the token path does not exist. The app and voice pods could not start in the test namespace (missing pull secret), so only their specs are verified.
readOnlyRootFilesystem
Enabled for db, app and voice. Deliberately not enabled for neo4j: live testing showed it shadows the image's default config.
RUNBOOK.md
Incident playbooks for db-down, neo4j-crash-loop, backup failure and expired ingress certificate, each drilled against a deliberately broken cluster.

Upgrading from 0.3.0: if the ServiceAccounts already exist from the raw manifests, Helm refuses with an ownership conflict. Install with --set serviceAccount.create=false (token mounting is still disabled), or delete the existing ServiceAccounts first.

NetworkPolicies

Restricts pod-to-pod traffic: ragleap-db only accepts connections from ragleap-app and ragleap-voice; ragleap-neo4j only from ragleap-app (ragleap-voice doesn't use neo4j in the current codebase).

db-allow-app-and-voice
Restricts port 5432 ingress on ragleap-db.
neo4j-allow-app-only
Restricts ports 7474/7687 ingress on ragleap-neo4j.

Important: kind's default CNI does not enforce NetworkPolicy at all — policies apply without error but traffic isn't actually blocked. Enforcement was live-tested end-to-end on a real kind + Calico cluster: unlabeled traffic genuinely blocked (timeout), correctly-labeled traffic genuinely allowed.

Ingress + TLS

Optional Helm chart Ingress + cert-manager Certificate, disabled by default via ingress.enabled: false.

helm install ragleap-ops ./ragleap-ops \
  --set ingress.enabled=true \
  --set ingress.hostname=your-real-domain.com \
  --set ingress.clusterIssuer=letsencrypt-prod

The full chain was live-tested end-to-end on a real kind cluster with a genuine NGINX Ingress Controller and cert-manager, using a self-signed ClusterIssuer — confirmed via openssl that the correct SNI-matched certificate was served, not a generic fallback. Production Let's Encrypt issuance was not live-tested (requires public DNS + a reachable port 80, which a local kind cluster can't provide).

Multi-environment deployment

Environment-specific overrides layer on top of the base values.yaml:

helm install ragleap-ops ./ragleap-ops -f values-dev.yaml       # local/dev
helm install ragleap-ops ./ragleap-ops -f values-staging.yaml   # staging
helm install ragleap-ops ./ragleap-ops -f values-prod.yaml      # production

app/voice replica counts are parameterized per environment; db/neo4j intentionally stay hardcoded at 1 replica — both are stateful, single-writer services on ReadWriteOnce PVCs. Rendered output confirmed to genuinely differ across all three overlays via helm template: dev (1/1, no ingress), staging (1/1, ingress on), prod (3/2, ingress on).

Autoscaling and disruption budgets (0.5.0)

Optional HorizontalPodAutoscaler and PodDisruptionBudget for the stateless app and voice workloads. Both are off by default; db and neo4j are stateful and are never autoscaled. With autoscaling off, the rendered chart is byte-identical to 0.4.1 for the default values and for values-prod.yaml.

app:
  resources:
    requests: { cpu: 200m, memory: 128Mi }   # required for CPU-percentage autoscaling
  autoscaling:
    enabled: true
    minReplicas: 2
    maxReplicas: 6                            # required
    targetCPUUtilizationPercentage: 70
  pdb:
    enabled: true
    minAvailable: 1
Scale-up and scale-down
Verified live on a kind cluster for app with a stand-in image: 1 → 4 replicas under CPU load, then 4 → 1 after the load stopped. The PDB allowed 0 disruptions at 1 replica and 3 at 4.
voice
Verified by helm template only. Its autoscaling was not run live because it needs the private application image. It has no readiness probe, and scaling it down cuts any live sessions on the removed pods.
Not tested
PDB behaviour during a real node drain, and anything beyond a single busy kind node. A PDB with minAvailable: 1 on a single replica blocks voluntary drains. Autoscaling needs metrics-server in the cluster.

Backup / Disaster Recovery

Scheduled backups for both db (Postgres) and neo4j.

db-backup-cronjob.yaml
Daily pg_dump against the live database — no downtime, Postgres handles concurrent dumps natively via MVCC.
neo4j-backup-cronjob.yaml
Scales ragleap-neo4j to 0, then runs neo4j-admin database dump against the now-unlocked PVC via a Job — Community Edition has no online/hot backup command (confirmed via --help; Enterprise-only).

Fail-loud by design: if a neo4j dump fails, the deployment stays scaled to 0 rather than auto-restoring — coupling uptime to backup success risks a silently-failing backup going unnoticed. Monitor CronJob/Job status explicitly and restore manually after investigating:

kubectl scale deployment ragleap-neo4j --replicas=1

Both patterns were live-tested against real running instances: pg_dump produced a valid dump; the neo4j scale-to-zero + dump pattern produced a genuine 257.8MiB, 36-file dump via a real Job. The CronJob wrapper itself (RBAC, scheduling) was validated structurally, not exercised via an actual cron trigger.

Known limitations

Drawn from a real deploy against a local kind cluster this session — including a bug that only surfaced under live testing, not from writing the manifests correctly the first time.

  • PVC storage sizes (5Gi) are placeholder defaults. docker-compose.yml's named volumes are unbounded, so there was no real size to translate from — size for your own capacity needs.
  • A liveness-probe timing bug was found and fixed via live testing. The original db probe's initialDelaySeconds: 10 was too short for first-boot image pull + PVC attach, causing kubelet to kill a container that was still legitimately starting. Fixed to initialDelaySeconds: 45, failureThreshold: 6, then re-verified with zero restarts.
  • GHCR image visibility. The private-to-public toggle wasn't reachable via the GitHub API in this environment, so app/voice authenticate via an imagePullSecret rather than relying on a public image.