Skip to main content

Frappe Helm Deployment Standard

Source: engineering/anshin-orchestrate (GitLab ID: 80) — orchestrate-core/helm/ and CLAUDE.md, Rev 2026-02-01

This 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

ItemValue
Helm chartfrappe/erpnext (v8+ recommended)
Default imagefrappe/erpnext:version-16 (public, for testing/fallback)
Production imageCustom CI-built image from registry.anshinhealth.net:443
Image pull policyIfNotPresent

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.

warning
Only list apps that exist in the Docker image under installApps

Listing 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:

KeyValue
mariadb-subchart.enabledfalse — old subchart, not used
mariadb-sts.enabledtrue — StatefulSet-backed, preferred for v8+
mariadb-sts.image.tag"11.8"
mariadb-sts.replicaCount1
mariadb-sts.persistence.storageClass"nfs"
mariadb-sts.persistence.size10Gi (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
Secrets — never hardcode in values files — use Infisical for all environments

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

danger
jobs.createSite.enabled MUST be false after initial setup

The 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:

  1. erpnext (core)
  2. payments
  3. Any vertical apps (frappe_whatsapp, etc.)
  4. Custom apps in dependency order (orchestrate before anshin_bff)
  5. anshin_bff last — 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:

  1. Removes old symlinks/directories from the shared PVC
  2. Copies fresh assets from the new image
  3. Generates sites/assets.json manifest 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
Removed annotations

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

EnvironmentNamespaceSite Name
Devorchestrate-devcore-dev.orchestrate.anshin.us
Stageorchestrate-stagecore-stage.orchestrate.anshin.us
Prodorchestrate-prodcore.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 cp on Windows — creates a literal C: directory on the pod
  • Always source env/bin/activate before running Python
  • Always include sites_path parameter in frappe.init()
  • Always call frappe.set_user("Administrator"), frappe.db.commit(), and frappe.destroy()
  • Scripts must be idempotent — check frappe.db.exists() before inserting

Document Control

RevDateAuthorDescription
1.02026-02-01Anshin EngineeringInitial release