Frappe Helm Deployment Standard
Source:
engineering/anshin-orchestrate(GitLab ID: 80) —orchestrate-core/helm/andCLAUDE.md, Rev 2026-02-01This document covers the standard Helm deployment pattern for Frappe/ERPNext on the Anshin Kubernetes cluster. All Frappe-based projects (Orchestrate Core, future ERP deployments) follow this pattern.
Chart and Image
| Item | Value |
|---|---|
| Helm chart | frappe/erpnext (v8+ recommended) |
| Default image | frappe/erpnext:version-16 (public, for testing/fallback) |
| Production image | Custom CI-built image from registry.anshinhealth.net:443 |
| Image pull policy | IfNotPresent |
The CI/CD pipeline overrides the image with --set image.repository=... --set image.tag=... during deployment. The default image in values.yaml is the upstream public image for reference/emergency use only.
installAppsListing apps that are not present in the built image will cause ModuleNotFoundError and break all Frappe operations including login.
Database
Frappe v16 requires MariaDB 11.8. The erpnext chart v8+ supports two MariaDB subchart options:
| Key | Value |
|---|---|
mariadb-subchart.enabled | false — old subchart, not used |
mariadb-sts.enabled | true — StatefulSet-backed, preferred for v8+ |
mariadb-sts.image.tag | "11.8" |
mariadb-sts.replicaCount | 1 |
mariadb-sts.persistence.storageClass | "nfs" |
mariadb-sts.persistence.size | 10Gi (dev) — size according to environment |
mariadb-subchart:
enabled: false
mariadb-sts:
enabled: true
image:
repository: mariadb
tag: "11.8"
replicaCount: 1
volumePermissions:
enabled: true
persistence:
enabled: true
storageClass: "nfs"
size: 10Gi
rootPassword hardcoded in values.yaml is dev only and temporary. All environments
(stage, prod) must use Infisical. All secrets go in Infisical and sync into K8s via the
InfisicalSecret CR — never via kubectl create secret or plaintext YAML.
# 1. Create secret in Infisical (project: anshin-orchestrate, env: prod, path: /mariadb)
# Key: MARIADB_ROOT_PASSWORD Value: <your-password>
# 2. Deploy InfisicalSecret CR to sync into K8s
apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalSecret
metadata:
name: orchestrate-mariadb-infisical
namespace: orchestrate-prod
spec:
authentication:
universalAuth:
credentialsRef:
name: infisical-universal-auth
namespace: infisical
app: anshin-orchestrate
environment: prod
secretsPath: /mariadb
resyncInterval: 60
managedSecretReference:
secretName: orchestrate-mariadb-secrets
secretNamespace: orchestrate-prod
# 3. Reference the synced secret in values.yaml
mariadb-sts:
existingSecret: orchestrate-mariadb-secrets
See Infisical Secrets Management for the full setup process.
Cache and Queue — Valkey
The erpnext chart v8+ offers Redis, Valkey, and Dragonfly as cache/queue backends. Anshin uses Valkey (Redis-compatible, chart default). Redis and Dragonfly are explicitly disabled.
redis-cache:
enabled: false
redis-queue:
enabled: false
dragonfly:
enabled: false
valkey-cache:
enabled: true
architecture: standalone
auth:
enabled: false
valkey-queue:
enabled: true
architecture: standalone
auth:
enabled: false
Persistence
persistence:
worker:
enabled: true
storageClass: "nfs"
size: 10Gi
logs:
enabled: true
storageClass: "nfs"
size: 2Gi
NFS storage class is used across the K3s cluster. Worker and logs volumes are separate PVCs.
jobs.createSite — Critical Lifecycle Rule
jobs.createSite.enabled MUST be false after initial setupThe Frappe Helm chart's createSite job runs bench new-site on every helm upgrade.
If enabled, this resets site configuration — OAuth clients, email accounts, system
settings, user roles — even though the MariaDB database persists on a PVC. This makes
the system appear broken after every deploy.
jobs:
createSite:
enabled: false # NEVER change this after initial setup
siteName: "core-dev.orchestrate.anshin.us"
installApps:
- "erpnext"
- "payments"
- "frappe_whatsapp"
- "orchestrate"
- "anshin_anna"
- "anshin_workflow"
- "anshin_channels"
- "anshin_commerce"
- "saas_guard"
- "anshin_bff"
- "helpdesk"
dbType: "mariadb"
App Install Order
Apps must be listed in dependency order:
erpnext(core)payments- Any vertical apps (
frappe_whatsapp, etc.) - Custom apps in dependency order (
orchestratebeforeanshin_bff) anshin_bfflast — requires all other apps to be installed first
For New Environments Only
# 1. Enable createSite for initial site creation (one-time)
helm upgrade --install ... --set jobs.createSite.enabled=true
# 2. After site creation, run post-install configuration
./scripts/run_post_install.sh dev
# 3. IMMEDIATELY set createSite back to false and commit
Post-Install Configuration
After every helm upgrade, the CI/CD pipeline automatically copies and runs scripts/post_install_config.py on the gunicorn pod. This script is idempotent (safe to run multiple times) and restores:
- NestedSet trees (
rebuild_tree()with 1 arg — see Frappe v16 Standards) - Gmail email account (outgoing notifications)
- OAuth2 client (frontend auth)
- System settings (CORS, login config)
- Admin user roles
If deploying manually outside CI/CD:
./scripts/run_post_install.sh dev # or stage, prod
Asset Refresh Init Container
The nginx container includes a custom refresh-assets init container that solves the symlink/PVC overlay issue where old assets persist across deploys.
nginx:
initContainers:
- name: refresh-assets
image: "registry.anshinhealth.net/engineering/anshin-orchestrate/core:dev-latest"
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
- |
echo "=== Refreshing assets from image to shared volume ==="
mkdir -p /home/frappe/frappe-bench/sites/assets
cd /home/frappe/frappe-bench/sites/assets
for app in frappe erpnext payments frappe_whatsapp orchestrate ...; do
rm -rf "$app"
cp -r "/home/frappe/frappe-bench/apps/$app/$app/public" "$app"
done
# Generate assets.json manifest (required for Frappe asset URL resolution)
...
volumeMounts:
- name: sites-dir
mountPath: /home/frappe/frappe-bench/sites
The init container:
- Removes old symlinks/directories from the shared PVC
- Copies fresh assets from the new image
- Generates
sites/assets.jsonmanifest so Frappe resolves asset URLs correctly
The CI/CD pipeline overrides the init container image tag to match the newly-built image: --set nginx.initContainers[0].image=...
Ingress
ingress:
enabled: true
className: "nginx"
annotations:
# Rate limiting
nginx.ingress.kubernetes.io/limit-rps: "200"
nginx.ingress.kubernetes.io/limit-connections: "100"
nginx.ingress.kubernetes.io/limit-burst-multiplier: "2"
nginx.ingress.kubernetes.io/limit-req-status-code: "429"
# CORS — list all frontend origins
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://app-dev.orchestrate.anshin.us,..."
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, OPTIONS"
nginx.ingress.kubernetes.io/cors-allow-headers: "Authorization, Content-Type, X-Frappe-CSRF-Token"
# Request size
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
hosts:
- host: core-dev.orchestrate.anshin.us
paths:
- path: /
pathType: Prefix
tls:
- secretName: wildcard-orchestrate-cert
hosts:
- core-dev.orchestrate.anshin.us
configuration-snippet and modsecurity-snippet annotations are removed — blocked by NGINX Ingress Controller v1.9+ (allow-snippet-annotations=false by default). Security headers must be added via ConfigMap.
cert-manager.io/cluster-issuer is removed — Anshin uses pre-provisioned wildcard certificates, not cert-manager auto-issuance.
Health Probes
Frappe v16 includes a built-in health endpoint at /api/method/ping (returns {"message": "pong"}).
livenessProbe:
httpGet:
path: /api/method/ping
port: 8080
initialDelaySeconds: 30
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 5
readinessProbe:
httpGet:
path: /api/method/ping
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /api/method/ping
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30 # Allows up to 5 minutes for startup
Resources
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 2Gi
Kubernetes Namespaces
| Environment | Namespace | Site Name |
|---|---|---|
| Dev | orchestrate-dev | core-dev.orchestrate.anshin.us |
| Stage | orchestrate-stage | core-stage.orchestrate.anshin.us |
| Prod | orchestrate-prod | core.orchestrate.anshin.us |
Executing Commands on Pods
The Standard Pattern (Required)
bench console does NOT work from kubectl exec (requires interactive TTY). Custom module imports fail on pods (ModuleNotFoundError for custom apps). Use the inline script via stdin pipe approach:
# Step 1: Copy script to pod
cat script.py | kubectl exec -i -n <ns> <pod> -- bash -c 'cat > /tmp/script.py'
# Step 2: Execute with Frappe context
kubectl exec -n <ns> <pod> -- bash -c '
cd /home/frappe/frappe-bench && source env/bin/activate && python -c "
import frappe
frappe.init(site=\"<site-name>\", sites_path=\"/home/frappe/frappe-bench/sites\")
frappe.connect()
frappe.set_user(\"Administrator\")
exec(open(\"/tmp/script.py\").read())
frappe.db.commit()
frappe.destroy()
"'
Critical Rules
- NEVER use
kubectl cpon Windows — creates a literalC:directory on the pod - Always
source env/bin/activatebefore running Python - Always include
sites_pathparameter infrappe.init() - Always call
frappe.set_user("Administrator"),frappe.db.commit(), andfrappe.destroy() - Scripts must be idempotent — check
frappe.db.exists()before inserting
Document Control
| Rev | Date | Author | Description |
|---|---|---|---|
| 1.0 | 2026-02-01 | Anshin Engineering | Initial release |