Frappe/ERPNext Free and Open Source Enterprise Resource Planning (ERP).
For evaluation setup simple in-cluster NFS server to make the nfs storage class with RWX capabilities available for use. Make sure you have default storage class (kubectl get sc) already on your cluster before creating nfs storage class.
kubectl create namespace nfs
helm repo add nfs-ganesha-server-and-external-provisioner https://kubernetes-sigs.github.io/nfs-ganesha-server-and-external-provisioner
helm upgrade --install -n nfs in-cluster nfs-ganesha-server-and-external-provisioner/nfs-server-provisioner --set 'storageClass.mountOptions={vers=4.1}' --set persistence.enabled=true --set persistence.size=8GiInstall ERPNext using the nfs storage class.
kubectl create namespace erpnext
helm repo add frappe https://helm.erpnext.com
helm upgrade --install frappe-bench --namespace erpnext frappe/erpnext --set persistence.worker.storageClass=nfs --set mariadb-sts.enabled=true- Introduction
- Parameters
- Requirements
- Configuration
- Installation
- Generate Additional Resources
- Uninstall the Chart
- Migrating from Bitnami Subcharts
This chart bootstraps a Frappe/ERPNext deployment on a Kubernetes cluster using the Helm package manager.
The following table lists the configurable parameters of the ERPNext chart and their default values.
Kubernetes Helm Chart for ERPNext and Frappe Framework Apps.
| Repository | Name | Version |
|---|---|---|
| https://charts.bitnami.com/bitnami | mariadb-subchart(mariadb) | 11.5.7 |
| https://charts.bitnami.com/bitnami | postgresql-subchart(postgresql) | 12.1.6 |
| https://charts.bitnami.com/bitnami | redis-cache(redis) | 17.15.2 |
| https://charts.bitnami.com/bitnami | redis-queue(redis) | 17.15.2 |
| https://valkey-io.github.io/valkey-helm | valkey-cache(valkey) | 0.9.3 |
| https://valkey-io.github.io/valkey-helm | valkey-queue(valkey) | 0.9.3 |
| oci://ghcr.io/dragonflydb/dragonfly/helm | dragonfly-cache(dragonfly) | v1.34.2 |
| oci://ghcr.io/dragonflydb/dragonfly/helm | dragonfly-queue(dragonfly) | v1.34.2 |
| Key | Type | Default | Description |
|---|---|---|---|
| dragonfly-cache.enabled | bool | false |
|
| dragonfly-queue.enabled | bool | false |
|
| dragonfly-queue.storage.enabled | bool | false |
|
| dragonfly-queue.storage.size | string | "8Gi" |
|
| externalRedis.cache | string | "" |
|
| externalRedis.queue | string | "" |
|
| fullnameOverride | string | "" |
|
| httproute.annotations | object | {} |
|
| httproute.enabled | bool | false |
|
| httproute.hostnames[0] | string | "erp.example.com" |
|
| httproute.name | string | "" |
|
| httproute.parentRefs[0].gatewayName | string | "public-gateway" |
|
| httproute.parentRefs[0].gatewayNamespace | string | "networking" |
|
| httproute.parentRefs[0].gatewaySectionName | string | "http" |
|
| httproute.rules[0].matches[0].path | string | "/" |
|
| httproute.rules[0].matches[0].pathType | string | "PathPrefix" |
|
| image.pullPolicy | string | "IfNotPresent" |
|
| image.repository | string | "frappe/erpnext" |
|
| image.tag | string | "v16.20.1" |
|
| imagePullSecrets | list | [] |
|
| ingress.annotations | object | {} |
|
| ingress.enabled | bool | false |
|
| ingress.hosts[0].host | string | "erp.cluster.local" |
|
| ingress.hosts[0].paths[0].path | string | "/" |
|
| ingress.hosts[0].paths[0].pathType | string | "ImplementationSpecific" |
|
| ingress.tls | list | [] |
|
| jobs.backup.affinity | object | {} |
|
| jobs.backup.backoffLimit | int | 0 |
|
| jobs.backup.enabled | bool | false |
|
| jobs.backup.nodeSelector | object | {} |
|
| jobs.backup.resources | object | {} |
|
| jobs.backup.siteName | string | "erp.cluster.local" |
|
| jobs.backup.tolerations | list | [] |
|
| jobs.backup.withFiles | bool | true |
|
| jobs.backupMultipleSites.affinity | object | {} |
|
| jobs.backupMultipleSites.backoffLimit | int | 0 |
|
| jobs.backupMultipleSites.enabled | bool | false |
|
| jobs.backupMultipleSites.nodeSelector | object | {} |
|
| jobs.backupMultipleSites.resources | object | {} |
|
| jobs.backupMultipleSites.sites[0].name | string | "erp.cluster.local" |
|
| jobs.backupMultipleSites.tolerations | list | [] |
|
| jobs.backupMultipleSites.withFiles | bool | true |
|
| jobs.clearCache.affinity | object | {} |
|
| jobs.clearCache.backoffLimit | int | 0 |
|
| jobs.clearCache.enabled | bool | false |
|
| jobs.clearCache.initContainer.deployment.name | string | "" |
|
| jobs.clearCache.initContainer.enabled | bool | false |
|
| jobs.clearCache.initContainer.image | string | "alpine/kubectl:1.35.0" |
|
| jobs.clearCache.initContainer.timeout | string | "600s" |
|
| jobs.clearCache.nodeSelector | object | {} |
|
| jobs.clearCache.redisFlush.enabled | bool | false |
|
| jobs.clearCache.resources | object | {} |
|
| jobs.clearCache.serviceAccountName | string | "" |
|
| jobs.clearCache.tolerations | list | [] |
|
| jobs.configure.affinity | object | {} |
|
| jobs.configure.args | list | [] |
|
| jobs.configure.backoffLimit | int | 0 |
|
| jobs.configure.command | list | [] |
|
| jobs.configure.enabled | bool | true |
|
| jobs.configure.envVars | list | [] |
|
| jobs.configure.fixVolume | bool | true |
|
| jobs.configure.nodeSelector | object | {} |
|
| jobs.configure.resources | object | {} |
|
| jobs.configure.tolerations | list | [] |
|
| jobs.createMultipleSites.adminExistingSecret | string | "" |
|
| jobs.createMultipleSites.adminExistingSecretKey | string | "password" |
|
| jobs.createMultipleSites.adminPassword | string | "changeit" |
|
| jobs.createMultipleSites.affinity | object | {} |
|
| jobs.createMultipleSites.backoffLimit | int | 0 |
|
| jobs.createMultipleSites.dbType | string | "mariadb" |
|
| jobs.createMultipleSites.enabled | bool | false |
|
| jobs.createMultipleSites.forceCreate | bool | false |
|
| jobs.createMultipleSites.nodeSelector | object | {} |
|
| jobs.createMultipleSites.resources | object | {} |
|
| jobs.createMultipleSites.sites[0].installApps[0] | string | "erpnext" |
|
| jobs.createMultipleSites.sites[0].name | string | "erp.cluster.local" |
|
| jobs.createMultipleSites.tolerations | list | [] |
|
| jobs.createSite.adminExistingSecret | string | "" |
|
| jobs.createSite.adminExistingSecretKey | string | "password" |
|
| jobs.createSite.adminPassword | string | "changeit" |
|
| jobs.createSite.affinity | object | {} |
|
| jobs.createSite.backoffLimit | int | 0 |
|
| jobs.createSite.dbType | string | "mariadb" |
|
| jobs.createSite.enabled | bool | false |
|
| jobs.createSite.forceCreate | bool | false |
|
| jobs.createSite.installApps[0] | string | "erpnext" |
|
| jobs.createSite.nodeSelector | object | {} |
|
| jobs.createSite.resources | object | {} |
|
| jobs.createSite.siteName | string | "erp.cluster.local" |
|
| jobs.createSite.tolerations | list | [] |
|
| jobs.custom.affinity | object | {} |
|
| jobs.custom.backoffLimit | int | 0 |
|
| jobs.custom.containers | list | [] |
|
| jobs.custom.enabled | bool | false |
|
| jobs.custom.initContainers | list | [] |
|
| jobs.custom.jobName | string | "" |
|
| jobs.custom.labels | object | {} |
|
| jobs.custom.nodeSelector | object | {} |
|
| jobs.custom.restartPolicy | string | "Never" |
|
| jobs.custom.tolerations | list | [] |
|
| jobs.custom.volumes | list | [] |
|
| jobs.dropMultipleSites.affinity | object | {} |
|
| jobs.dropMultipleSites.backoffLimit | int | 0 |
|
| jobs.dropMultipleSites.enabled | bool | false |
|
| jobs.dropMultipleSites.forced | bool | false |
|
| jobs.dropMultipleSites.nodeSelector | object | {} |
|
| jobs.dropMultipleSites.resources | object | {} |
|
| jobs.dropMultipleSites.sites[0].name | string | "site1.example.com" |
|
| jobs.dropMultipleSites.sites[1].name | string | "site2.example.com" |
|
| jobs.dropMultipleSites.tolerations | list | [] |
|
| jobs.dropSite.affinity | object | {} |
|
| jobs.dropSite.backoffLimit | int | 0 |
|
| jobs.dropSite.enabled | bool | false |
|
| jobs.dropSite.forced | bool | false |
|
| jobs.dropSite.nodeSelector | object | {} |
|
| jobs.dropSite.resources | object | {} |
|
| jobs.dropSite.siteName | string | "erp.cluster.local" |
|
| jobs.dropSite.tolerations | list | [] |
|
| jobs.migrate.affinity | object | {} |
|
| jobs.migrate.backoffLimit | int | 0 |
|
| jobs.migrate.enabled | bool | false |
|
| jobs.migrate.nodeSelector | object | {} |
|
| jobs.migrate.resources | object | {} |
|
| jobs.migrate.siteName | string | "erp.cluster.local" |
|
| jobs.migrate.skipFailing | bool | false |
|
| jobs.migrate.tolerations | list | [] |
|
| jobs.migrateMultipleSites.affinity | object | {} |
|
| jobs.migrateMultipleSites.backoffLimit | int | 0 |
|
| jobs.migrateMultipleSites.enabled | bool | false |
|
| jobs.migrateMultipleSites.nodeSelector | object | {} |
|
| jobs.migrateMultipleSites.resources | object | {} |
|
| jobs.migrateMultipleSites.sites[0].name | string | "erp.cluster.local" |
|
| jobs.migrateMultipleSites.skipFailing | bool | false |
|
| jobs.migrateMultipleSites.tolerations | list | [] |
|
| jobs.volumePermissions.affinity | object | {} |
|
| jobs.volumePermissions.backoffLimit | int | 0 |
|
| jobs.volumePermissions.enabled | bool | false |
|
| jobs.volumePermissions.nodeSelector | object | {} |
|
| jobs.volumePermissions.resources | object | {} |
|
| jobs.volumePermissions.tolerations | list | [] |
|
| mariadb-sts.enabled | bool | false |
|
| mariadb-sts.image.pullPolicy | string | "IfNotPresent" |
|
| mariadb-sts.image.repository | string | "mariadb" |
|
| mariadb-sts.image.tag | string | "10.6" |
|
| mariadb-sts.myCnf | string | "[mysqld]\nskip-character-set-client-handshake\nskip-innodb-read-only-compressed\ncharacter-set-server=utf8mb4\ncollation-server=utf8mb4_unicode_ci\n" |
|
| mariadb-sts.persistence.size | string | "8Gi" |
|
| mariadb-sts.resources | object | {} |
|
| mariadb-sts.rootPassword | string | "changeit" |
|
| mariadb-subchart.enabled | bool | false |
|
| mariadb-subchart.image.repository | string | "bitnamilegacy/mariadb" |
|
| mariadb-subchart.image.tag | string | "10.6.17-debian-11-r10" |
|
| mariadb.enabled | bool | false |
|
| nameOverride | string | "" |
|
| nginx.affinity | object | {} |
|
| nginx.autoscaling.enabled | bool | false |
|
| nginx.autoscaling.maxReplicas | int | 3 |
|
| nginx.autoscaling.minReplicas | int | 1 |
|
| nginx.autoscaling.targetCPU | int | 75 |
|
| nginx.autoscaling.targetMemory | int | 75 |
|
| nginx.defaultTopologySpread.maxSkew | int | 1 |
|
| nginx.defaultTopologySpread.topologyKey | string | "kubernetes.io/hostname" |
|
| nginx.defaultTopologySpread.whenUnsatisfiable | string | "DoNotSchedule" |
|
| nginx.envVars | list | [] |
|
| nginx.environment.clientMaxBodySize | string | "50m" |
|
| nginx.environment.frappeSiteNameHeader | string | "$host" |
|
| nginx.environment.proxyReadTimeout | string | "120" |
|
| nginx.environment.upstreamRealIPAddress | string | "127.0.0.1" |
|
| nginx.environment.upstreamRealIPHeader | string | "X-Forwarded-For" |
|
| nginx.environment.upstreamRealIPRecursive | string | "off" |
|
| nginx.initContainers | list | [] |
|
| nginx.livenessProbe.initialDelaySeconds | int | 5 |
|
| nginx.livenessProbe.periodSeconds | int | 10 |
|
| nginx.livenessProbe.tcpSocket.port | int | 8080 |
|
| nginx.nodeSelector | object | {} |
|
| nginx.podAnnotations | object | {} |
|
| nginx.readinessProbe.initialDelaySeconds | int | 5 |
|
| nginx.readinessProbe.periodSeconds | int | 10 |
|
| nginx.readinessProbe.tcpSocket.port | int | 8080 |
|
| nginx.replicaCount | int | 1 |
|
| nginx.resources | object | {} |
|
| nginx.service.port | int | 8080 |
|
| nginx.service.type | string | "ClusterIP" |
|
| nginx.sidecars | list | [] |
|
| nginx.tolerations | list | [] |
|
| persistence.logs.accessModes[0] | string | "ReadWriteMany" |
|
| persistence.logs.enabled | bool | false |
|
| persistence.logs.size | string | "8Gi" |
|
| persistence.worker.accessModes[0] | string | "ReadWriteMany" |
|
| persistence.worker.enabled | bool | true |
|
| persistence.worker.size | string | "8Gi" |
|
| podSecurityContext.supplementalGroups[0] | int | 1000 |
|
| postgresql-sts.enabled | bool | false |
|
| postgresql-sts.image.pullPolicy | string | "IfNotPresent" |
|
| postgresql-sts.image.repository | string | "postgres" |
|
| postgresql-sts.image.tag | string | "15" |
|
| postgresql-sts.persistence.size | string | "8Gi" |
|
| postgresql-sts.postgresPassword | string | "changeit" |
|
| postgresql-sts.postgresUser | string | "postgres" |
|
| postgresql-sts.resources | object | {} |
|
| postgresql-subchart.enabled | bool | false |
|
| postgresql-subchart.image.repository | string | "bitnamilegacy/postgresql" |
|
| postgresql-subchart.image.tag | string | "14" |
|
| postgresql.enabled | bool | false |
|
| redis-cache.enabled | bool | false |
|
| redis-cache.image.repository | string | "bitnamilegacy/redis" |
|
| redis-cache.image.tag | string | "7.0" |
|
| redis-queue.enabled | bool | false |
|
| redis-queue.image.repository | string | "bitnamilegacy/redis" |
|
| redis-queue.image.tag | string | "7.0" |
|
| securityContext.capabilities.add[0] | string | "CAP_CHOWN" |
|
| serviceAccount.create | bool | true |
|
| socketio.affinity | object | {} |
|
| socketio.autoscaling.enabled | bool | false |
|
| socketio.autoscaling.maxReplicas | int | 3 |
|
| socketio.autoscaling.minReplicas | int | 1 |
|
| socketio.autoscaling.targetCPU | int | 75 |
|
| socketio.autoscaling.targetMemory | int | 75 |
|
| socketio.envVars | list | [] |
|
| socketio.initContainers | list | [] |
|
| socketio.livenessProbe.initialDelaySeconds | int | 5 |
|
| socketio.livenessProbe.periodSeconds | int | 10 |
|
| socketio.livenessProbe.tcpSocket.port | int | 9000 |
|
| socketio.nodeSelector | object | {} |
|
| socketio.podAnnotations | object | {} |
|
| socketio.readinessProbe.initialDelaySeconds | int | 5 |
|
| socketio.readinessProbe.periodSeconds | int | 10 |
|
| socketio.readinessProbe.tcpSocket.port | int | 9000 |
|
| socketio.replicaCount | int | 1 |
|
| socketio.resources | object | {} |
|
| socketio.service.port | int | 9000 |
|
| socketio.service.type | string | "ClusterIP" |
|
| socketio.sidecars | list | [] |
|
| socketio.tolerations | list | [] |
|
| valkey-cache.enabled | bool | true |
|
| valkey-cache.image.repository | string | "valkey/valkey" |
|
| valkey-cache.image.tag | string | "7.2" |
|
| valkey-queue.enabled | bool | true |
|
| valkey-queue.image.repository | string | "valkey/valkey" |
|
| valkey-queue.image.tag | string | "7.2" |
|
| worker.default.affinity | object | {} |
|
| worker.default.autoscaling.enabled | bool | false |
|
| worker.default.autoscaling.maxReplicas | int | 3 |
|
| worker.default.autoscaling.minReplicas | int | 1 |
|
| worker.default.autoscaling.targetCPU | int | 75 |
|
| worker.default.autoscaling.targetMemory | int | 75 |
|
| worker.default.envVars | list | [] |
|
| worker.default.initContainers | list | [] |
|
| worker.default.livenessProbe.override | bool | false |
|
| worker.default.livenessProbe.probe | object | {} |
|
| worker.default.nodeSelector | object | {} |
|
| worker.default.podAnnotations | object | {} |
|
| worker.default.readinessProbe.override | bool | false |
|
| worker.default.readinessProbe.probe | object | {} |
|
| worker.default.replicaCount | int | 1 |
|
| worker.default.resources | object | {} |
|
| worker.default.sidecars | list | [] |
|
| worker.default.tolerations | list | [] |
|
| worker.defaultTopologySpread.maxSkew | int | 1 |
|
| worker.defaultTopologySpread.topologyKey | string | "kubernetes.io/hostname" |
|
| worker.defaultTopologySpread.whenUnsatisfiable | string | "DoNotSchedule" |
|
| worker.gunicorn.affinity | object | {} |
|
| worker.gunicorn.args | list | [] |
|
| worker.gunicorn.autoscaling.enabled | bool | false |
|
| worker.gunicorn.autoscaling.maxReplicas | int | 3 |
|
| worker.gunicorn.autoscaling.minReplicas | int | 1 |
|
| worker.gunicorn.autoscaling.targetCPU | int | 75 |
|
| worker.gunicorn.autoscaling.targetMemory | int | 75 |
|
| worker.gunicorn.envVars | list | [] |
|
| worker.gunicorn.initContainers | list | [] |
|
| worker.gunicorn.livenessProbe.initialDelaySeconds | int | 5 |
|
| worker.gunicorn.livenessProbe.periodSeconds | int | 10 |
|
| worker.gunicorn.livenessProbe.tcpSocket.port | int | 8000 |
|
| worker.gunicorn.nodeSelector | object | {} |
|
| worker.gunicorn.podAnnotations | object | {} |
|
| worker.gunicorn.readinessProbe.initialDelaySeconds | int | 5 |
|
| worker.gunicorn.readinessProbe.periodSeconds | int | 10 |
|
| worker.gunicorn.readinessProbe.tcpSocket.port | int | 8000 |
|
| worker.gunicorn.replicaCount | int | 1 |
|
| worker.gunicorn.resources | object | {} |
|
| worker.gunicorn.service.port | int | 8000 |
|
| worker.gunicorn.service.type | string | "ClusterIP" |
|
| worker.gunicorn.sidecars | list | [] |
|
| worker.gunicorn.tolerations | list | [] |
|
| worker.healthProbe | string | `"exec:\n command:\n - bash\n - -c\n - | -\n echo "Pinging backing services";\n {{- if (index .Values "mariadb-sts").enabled }}\n wait-for-it {{ include "erpnext.fullname" . }}-mariadb-sts:3306 -t 1;\n {{- else if (index .Values "postgresql-sts").enabled }}\n wait-for-it {{ include "erpnext.fullname" . }}-postgresql-sts:5432 -t 1;\n {{- else if or .Values.mariadb.enabled (get .Values "mariadb-subchart").enabled }}\n (\n wait-for-it {{ .Release.Name }}-mariadb-subchart:3306 -t 0 |
| worker.long.affinity | object | {} |
|
| worker.long.autoscaling.enabled | bool | false |
|
| worker.long.autoscaling.maxReplicas | int | 3 |
|
| worker.long.autoscaling.minReplicas | int | 1 |
|
| worker.long.autoscaling.targetCPU | int | 75 |
|
| worker.long.autoscaling.targetMemory | int | 75 |
|
| worker.long.envVars | list | [] |
|
| worker.long.initContainers | list | [] |
|
| worker.long.livenessProbe.override | bool | false |
|
| worker.long.livenessProbe.probe | object | {} |
|
| worker.long.nodeSelector | object | {} |
|
| worker.long.podAnnotations | object | {} |
|
| worker.long.readinessProbe.override | bool | false |
|
| worker.long.readinessProbe.probe | object | {} |
|
| worker.long.replicaCount | int | 1 |
|
| worker.long.resources | object | {} |
|
| worker.long.sidecars | list | [] |
|
| worker.long.tolerations | list | [] |
|
| worker.scheduler.affinity | object | {} |
|
| worker.scheduler.envVars | list | [] |
|
| worker.scheduler.initContainers | list | [] |
|
| worker.scheduler.livenessProbe.override | bool | false |
|
| worker.scheduler.livenessProbe.probe | object | {} |
|
| worker.scheduler.nodeSelector | object | {} |
|
| worker.scheduler.podAnnotations | object | {} |
|
| worker.scheduler.readinessProbe.override | bool | false |
|
| worker.scheduler.readinessProbe.probe | object | {} |
|
| worker.scheduler.replicaCount | int | 1 |
|
| worker.scheduler.resources | object | {} |
|
| worker.scheduler.sidecars | list | [] |
|
| worker.scheduler.tolerations | list | [] |
|
| worker.short.affinity | object | {} |
|
| worker.short.autoscaling.enabled | bool | false |
|
| worker.short.autoscaling.maxReplicas | int | 3 |
|
| worker.short.autoscaling.minReplicas | int | 1 |
|
| worker.short.autoscaling.targetCPU | int | 75 |
|
| worker.short.autoscaling.targetMemory | int | 75 |
|
| worker.short.envVars | list | [] |
|
| worker.short.initContainers | list | [] |
|
| worker.short.livenessProbe.override | bool | false |
|
| worker.short.livenessProbe.probe | object | {} |
|
| worker.short.nodeSelector | object | {} |
|
| worker.short.podAnnotations | object | {} |
|
| worker.short.readinessProbe.override | bool | false |
|
| worker.short.readinessProbe.probe | object | {} |
|
| worker.short.replicaCount | int | 1 |
|
| worker.short.resources | object | {} |
|
| worker.short.sidecars | list | [] |
|
| worker.short.tolerations | list | [] |
The above parameters map to the env variables defined in frappe_docker. For more information please refer to the frappe_docker images documentation.
Frappe framework sites are stored in shared volume that needs to be accessed by multiple pods. Read more about Storage Classes. Here are some alternatives available for RWX volumes.
- AWS EFS: Managed shared filesystem by Amazon.
- Google Filestore: Managed shared filesystem by Google.
- AzureFile: Managed shared filesystem by Microsoft.
- External NFS Server: Provisioner based on NFS server setup outside cluster. Separately hosted NFS server is needed in this case.
- In-cluster NFS Server: Provisioner based on in-cluster NFS server.
- More Cloud Native Storage alternatives: Make sure the
storageclasshasReadWriteManyaccess mode to use it as storage for sites.
Note: If you are running a single-node cluster or deploying to a specific node using node affinity, you can use ReadWriteOnce (RWO) access mode if your storage class supports it. This is because all pods will be scheduled on the same node, eliminating the need for multi-node access.
Database is not deployed by default and must be explicitly enabled.
Note: ERPNext requires MariaDB. PostgreSQL can be used for custom Frappe apps that support it.
Recommended alternatives as per priority:
- Managed DB: Recommended AWS MariaDB RDS.
- Self hosted MariaDB: Self hosted mariadb server setup for Debian or Ubuntu.
- Bitnami Subcharts: For backward compatibility, the Bitnami MariaDB and PostgreSQL subcharts can be enabled. See the Using Bitnami Subcharts (Legacy) section.
By default, this chart deploys two Valkey instances, one for caching and one for the queue. Valkey is used as an in-memory database, and having it in the cluster provides the lowest latency.
For backward compatibility or other requirements, DragonflyDB or the Bitnami Redis subchart can be enabled instead. See the Using Bitnami Subcharts (Legacy) section.
Alternatively, any managed Redis-compatible service can be used by providing the connection details in your values.yaml.
Create a custom-values.yaml file to override the default settings. Below are examples for common configurations.
Make following changes to custom-values.yaml:
persistence:
worker:
storageClass: "rook-cephfs"Make sure the storage class called rook-cephfs is available on the cluster.
Make following changes to custom-values.yaml:
persistence:
worker:
existingClaim: existing-sitesMake sure the PVC called existing-sites exists in the namespace.
Make following changes to custom-values.yaml:
persistence:
worker:
accessModes:
- ReadWriteMany # default modeYou can configure this to use ReadWriteOnce (RWO) if you are running a single-node cluster or deploying to a specific node using node affinity.
To use an external database (like Amazon RDS), disable the built-in and subchart databases and provide the connection details.
# Disable all in-cluster databases
mariadb-sts:
enabled: false
postgresql-sts:
enabled: false
mariadb-subchart:
enabled: false
postgresql-subchart:
enabled: false
dbHost: "1.2.3.4"
dbPort: "3306"
dbRootUser: "admin"
# Use dbExistingSecret for production environments
dbExistingSecret: "my-external-db-secret"
dbExistingSecretPasswordKey: "password"Note for Upgrading Users: If you are upgrading from a previous version where you used the default Bitnami subchart, you must now explicitly set
mariadb-subchart: true(ormariadb.enabled: truefor backward compatibility) in yourcustom-values.yamlto prevent Helm from disabling it during the upgrade.
To use the classic Bitnami subcharts for the database or cache/queue, disable the new built-in components and enable the corresponding subcharts.
# Disable new built-in MariaDB and Valkey
mariadb-sts:
enabled: false
valkey-cache:
enabled: false
valkey-queue:
enabled: false
# Enable and configure classic Bitnami subcharts
mariadb-subchart:
enabled: true
# Pass values to the bitnami/mariadb subchart here
# primary:
# persistence:
# size: 10Gi
redis-cache:
enabled: true
# Pass values to the bitnami/redis subchart here
redis-queue:
enabled: true
# Pass values to the bitnami/redis subchart hereCreate namespace for erpnext
kubectl create namespace erpnextAdd helm frappe helm repo
helm repo add frappe https://helm.erpnext.comInstall helm chart and create release
helm install frappe-bench -n erpnext -f custom-values.yaml frappe/erpnextFor rest of the document frappe-bench is treated as the helm release name and custom-values.yaml file will contain required values to override.
These resources can be generated and created along with installation and upgrade of the helm release.
Instead of doing that we can have better control if we use helm template command and generate the required resources. That way we get yaml files that can also be committed to gitops repo. Generating same job multiple times will not cause any problem as the job names will have Unix timestamp.
Following sections discuss different resources that can be created using helm template command.
Make following changes to custom-values.yaml:
jobs:
createSite:
enabled: true
siteName: "erp.example.com"
adminPassword: "secret"Note: erp.example.com must be configured DNS entry and change the adminPassword to something more secure.
Generate Job YAML
helm template frappe-bench -n erpnext frappe/erpnext -f custom-values.yaml -s templates/job-create-site.yaml > create-new-site-job.yamlCreate Job resource
kubectl apply -f create-new-site-job.yamlMake following changes to custom-values.yaml:
ingress:
enabled: true
ingressName: "erp-example-com"
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
hosts:
- host: erp.example.com
paths:
- path: /
pathType: ImplementationSpecific
tls:
- secretName: erp-example-com-tls
hosts:
- erp.example.comNote:
erp.example.commust be configured DNS entry.- Change
annotationsas per requirement. - Change
secretNameas per convenience.
Generate Ingress YAML
helm template frappe-bench -n erpnext frappe/erpnext -f custom-values.yaml -s templates/ingress.yaml > ingress.yamlCreate Ingress resource
kubectl apply -f ingress.yamlIf your cluster is using GatewayAPI, make the following changes to custom-values.yaml:
httproute:
enabled: true
name: "erp-example-com-httproute"
annotations: {}
parentRefs:
- gatewayName: "public-gateway" # name of your Gateway resource
gatewayNamespace: "networking" # namespace where the Gateway is defined
gatewaySectionName: "http" # listener name (optional), check your gateway requirements
hostnames:
- "erp.example.com"
rules:
- matches:
- pathType: "PathPrefix"
path: "/"Note:
- A Gateway resource (e.g.,
public-gateway) must already exist in the cluster. erp.example.commust be an existing and configured DNS entry.gatewayNamespace,gatewaySectionName, and other fields can be customized as needed.- Change
annotationsas per requirement, if any.
Generate the HTTPRoute YAML
helm template frappe-bench -n erpnext frappe/erpnext -f custom-values.yaml -s templates/httproute.yaml > httproute.yamlCreate HTTPRoute resource
kubectl apply -f httproute.yamlMake following changes to custom-values.yaml:
jobs:
backup:
enabled: true
siteName: "erp.example.com"
withFiles: true
push:
enabled: false
bucket: "erpnext"
region: "us-east-1"
accessKey: "ACCESSKEY"
secretKey: "SECRETKEY"
endpoint: http://store.storage.svc.cluster.localNote:
- Site
erp.example.commust exist. - To push backup to S3, enter S3 credentials and set
jobs.backup.push.enabledtotrue.
Generate Job YAML
helm template frappe-bench -n erpnext frappe/erpnext -f custom-values.yaml -s templates/job-backup.yaml > job-backup.yamlCreate Job resource
kubectl apply -f job-backup.yamlMake following changes to custom-values.yaml:
jobs:
migrate:
enabled: true
siteName: "erp.example.com"Note: Site erp.example.com must exist.
Generate Job YAML
helm template frappe-bench -n erpnext frappe/erpnext -f custom-values.yaml -s templates/job-migrate-site.yaml > job-migrate-site.yamlCreate Job resource
kubectl apply -f job-migrate-site.yamlMake following changes to custom-values.yaml:
jobs:
dropSite:
enabled: true
siteName: "erp.example.com"Note: Site erp.example.com must exist.
Generate Job YAML
helm template frappe-bench -n erpnext frappe/erpnext -f custom-values.yaml -s templates/job-drop-site.yaml > job-drop-site.yamlCreate Job resource
kubectl apply -f job-drop-site.yamlBy default this job configures service hosts automatically as per name of the helm release.
To manually set hosts make following changes to custom-values.yaml:
# Disable in-cluster databases and caches
mariadb-sts:
enabled: false
valkey-cache:
enabled: false
valkey-queue:
enabled: false
# Configure external services
dbHost: "db-instance.123456789012.us-east-1.rds.amazonaws.com"
dbPort: 3306
externalRedis:
cache: "redis://redis-cache.7abc2d.0001.usw2.cache.amazonaws.com:6379"
queue: "redis://redis-queue.7abc2d.0001.usw2.cache.amazonaws.com:6379"
jobs:
configure:
enabled: trueNotes:
- Change the hosts as per configuration
- If
jobs.configure.fixVolumeis set totrueit will run command as root to change ownership of files and directories in volume.
Generate Job YAML
helm template frappe-bench -n erpnext frappe/erpnext -f custom-values.yaml -s templates/job-configure-bench.yaml > job-configure-bench.yamlCreate Job resource
kubectl apply -f job-configure-bench.yamlMake following changes to custom-values.yaml:
jobs:
volumePermissions:
enabled: trueGenerate Job YAML
helm template frappe-bench -n erpnext frappe/erpnext -f custom-values.yaml -s templates/job-fix-volume-permission.yaml > job-fix-volume-permission.yamlCreate Job resource
kubectl apply -f job-fix-volume-permission.yamlMake following changes to custom-values.yaml:
jobs:
clearCache:
enabled: true
# Configure initContainer to wait for rollout
initContainer:
enabled: true
image: alpine/kubectl:1.35.0
deployment:
name: "gunicorn"
timeout: 600sGenerate Job YAML
helm template frappe-bench -n erpnext frappe/erpnext -f custom-values.yaml -s templates/job-clear-cache.yaml > job-clear-cache.yamlCreate Job resource
kubectl apply -f job-clear-cache.yamlTo uninstall/delete the frappe-bench release:
helm --namespace erpnext delete frappe-benchThe command removes all the Kubernetes components installed by the chart and deletes the release.
IMPORTANT: If you are upgrading from a previous version (7.x or older) and relied on the default in-cluster database (Bitnami MariaDB or PostgreSQL), you must read this section and the MIGRATION.md guide before performing
helm upgrade. The new chart version changes the default behavior, and failing to update yourcustom-values.yamlcan lead to the accidental uninstallation of your database and potential data loss.