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.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.
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.yamlpgvector/pgvector:pg16. PVC-backed, schema loaded via a ConfigMap generated from the real db/schema.sql.app-deployment.yamlghcr.io/antonyrag/ragleap-app:latest, built from the repo-root Dockerfile. Waits on db via an init container polling pg_isready.voice-deployment.yamlapp, command overridden to python3 -m channels.voice.server, listens on 8765.neo4j-deployment.yamlneo4j: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.
ServiceAccountsdb, 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.readOnlyRootFilesystemdb, app and voice. Deliberately not enabled for neo4j: live testing showed it shadows the image's default config.RUNBOOK.mdUpgrading 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-voiceragleap-db.neo4j-allow-app-onlyragleap-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-downapp 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.voicehelm 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 testedminAvailable: 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.yamlpg_dump against the live database — no downtime, Postgres handles concurrent dumps natively via MVCC.neo4j-backup-cronjob.yamlragleap-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
dbprobe'sinitialDelaySeconds: 10was too short for first-boot image pull + PVC attach, causing kubelet to kill a container that was still legitimately starting. Fixed toinitialDelaySeconds: 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/voiceauthenticate via animagePullSecretrather than relying on a public image.