ERPNext is a beautiful piece of open source (GPL licensed) work - intended to offer an alternative to the encumbered and very enterprise SAP & ERP solutions that typically dominate the business world.
However, given the nature of an OSS ERP project, a slow but natural rate of adoption & popularity will ensure, but for now the kubernetes/helm documentation are quite raw. Community and developer resources are also lacking due to reasons that will likely just resolve, as the project matures and gets more widely adopted.
We've had a very recent release of K3d v4.0 (mid-Jan 2021) which I had been waiting keenly to try out. It just felt like a perfect matching, but understandably lacking resources pairing the two.
If you just want to blindly copy+paste commands, getting your instance operational ASAP:
- create a local k3s cluster by using k3d
- add some namespaces to said cluster
- prepare kubernetes resources & helm values to our filesytem
- install some helm charts to said cluster
- declare a persistent volume claim & secret for our cluster
- run a kubernetes job to create ERPNext site
- setup an ingress to route our ERPNext site through the LoadBalancer
- suss out the joys of ERPNext
It is assumed you already have kubectl & Docker installed, and that you're running a Unix based OS.
k3d is a helper that lets you easily create k3s clusters, using a docker daemon
curl -s https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash
v4.10 was latest at time of writing
We will be installing the ERPNext stack by using their official helm chart
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
v3.5.1 was latest at time of writing
Consider using kubectx or setting your own zsh/bash aliases to easily switch kubectl context
k3d cluster create erpnext -v /opt/local-path-provisioner:/opt/local-path-provisioner kubectl config use-context k3d-erpnext
A volume is required for the cluster because we will be using the k3s built-in local-path persistence
Your kubecontext gets updated with the second command, meaning now when we run
kubectl it will default to our new K3s cluster.
Firstly we'll work within a newly created directory because
~ is already a mess
mkdir erpnext-stuff && cd erpnext-stuff
helm repo add bitnami https://charts.bitnami.com/bitnami helm repo add frappe https://helm.erpnext.com helm repo update
Create two namespaces to separate our database from ERPNext.
kubectl create ns mariadb kubectl create ns erpnext
Namespaces are a good way to separate concerns, but not a hard-requirement
Save each Kubernetes resource inside the recently created
Do not run kubectl apply for now
apiVersion: v1 kind: PersistentVolumeClaim metadata: labels: app: erpnext name: erpnext-pvc namespace: erpnext spec: accessModes: - ReadWriteOnce resources: requests: storage: 4Gi storageClassName: local-path
Despite the ERPNext helm chart creating a PVC, we actually want to avoid using their PVC because the
accessMode is hardcoded to
RWX - ReadWriteMany.
When using K3s,
RWX is not possible with the built-in storage controller 'local-path'. It only supports
RWO - ReadWriteOnce or below, and this is sufficient for our needs.
Dumbing down what
RWO - ReadWriteOnceis, the volume can be mounted to support writes one node at a time
Given we've provisioned our K3s cluster with a single node (default)
RWOwill suffice for our needs in place of
Kubernetes provide a list of possible storage classes along with their supported access modes, but this is mostly unrelated for our goal today.
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress annotations: ingress.kubernetes.io/ssl-redirect: "false" spec: ingressClassName: traefik rules: - host: "localhost" http: paths: - path: / pathType: Prefix backend: service: name: erpnext port: number: 80
The ingress resource makes our built-in ingress-controller (ie. web server) traefik aware of a routing rule.
In our case we're telling traefik that
http://localhost/ will route through a service called
erpnext being a service which will be provisioned when we install the ERPNext helm chart.
Still unsure what an Ingress is? think of it like a virtualhost when using Apache, or an nginx
apiVersion: v1 data: password: c29tZVNlY3VyZVBhc3N3b3Jk kind: Secret metadata: name: mariadb-root-password type: Opaque
This secret will hold the password of the database user which ERPNext will use to create sites, and perform queries with.
c29tZVNlY3VyZVBhc3N3b3Jk is base64 for
someSecurePasword and it's the same password MariaDB will be told to use as root password, when we install later.
Feel free to change as you see fit, and obviously do not use my defaults in a real or production environment.
apiVersion: batch/v1 kind: Job metadata: name: create-erp-site spec: backoffLimit: 0 template: spec: securityContext: supplementalGroups:  containers: - name: create-site image: frappe/erpnext-worker:v12.17.0 args: ["new"] imagePullPolicy: IfNotPresent volumeMounts: - name: sites-dir mountPath: /home/frappe/frappe-bench/sites env: - name: "SITE_NAME" value: "localhost" - name: "DB_ROOT_USER" value: root - name: "MYSQL_ROOT_PASSWORD" valueFrom: secretKeyRef: key: password name: mariadb-root-password - name: "ADMIN_PASSWORD" value: "bigchungus" - name: "INSTALL_APPS" value: "erpnext" restartPolicy: Never volumes: - name: sites-dir persistentVolumeClaim: claimName: erpnext-pvc readOnly: false
As mentioned before, ERPNext is multi-tenanted. You can run many sites, and sites can have many companies.
One database is created per site, and there's also some configuration files created for the ERPNext setup to resolve sites to databases and beyond.
The 'create-site' job is the recommended way to provision a new 'site' to your ERPNext setup.
Paths of interest
spec.template.spec.containers.image- should match version used in helm chart
spec.template.spec.containers.volumeMounts- volume required for ERPNext to resolve hostnames to databases, and other meta
SITE_NAMEis the FQDN where this ERPNext site is destined for
ADMIN_PASSWORDwe will use this to login with later
spec.template.spec.volumes- volume mount based on our
auth: rootPassword: "someSecurePassword" primary: configuration: |- [mysqld] character-set-client-handshake=FALSE skip-name-resolve explicit_defaults_for_timestamp basedir=/opt/bitnami/mariadb plugin_dir=/opt/bitnami/mariadb/plugin port=3306 socket=/opt/bitnami/mariadb/tmp/mysql.sock tmpdir=/opt/bitnami/mariadb/tmp max_allowed_packet=16M bind-address=0.0.0.0 pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid log-error=/opt/bitnami/mariadb/logs/mysqld.log character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci [client] port=3306 socket=/opt/bitnami/mariadb/tmp/mysql.sock default-character-set=utf8mb4 plugin_dir=/opt/bitnami/mariadb/plugin [manager] port=3306 socket=/opt/bitnami/mariadb/tmp/mysql.sock pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid
ERPNext indicate your MariaDB instance should explicitly use this configuration. I'm going to assume they're primarily wanting you to have a
utf8mb4 happy setup.
You may notice the ERPNext helm chart instructions at https://helm.erpnext.com/prepare-kubernetes/mariadb are slightly different to my values above.
This is because their documentation revolves around an older mariadb chart version. The newer chart version does not enable slave by default now too, so our config is simplified.
replicaCount: 1 mariadbHost: "mariadb.mariadb.svc.cluster.local" persistence: enabled: false existingClaim: "erpnext-pvc"
- erpnext helm chart default values
- rando handbook with useful information
- frappe/helm - github
- erpnext helm docs
- Declare the PVC to your cluster. By default the PVC will not be provisioned until required (ie. container mounts it)
kubectl apply --namespace erpnext -f pvc.yaml
- Install MariaDB with our specific server configuration, and root password.
--waitwill force the process to hang until all pods & services are healthy
helm install mariadb --namespace mariadb bitnami/mariadb --version 9.3.1 -f maria-db-values.yaml --wait
- Install ERPNext. All services and pods will be deployed essentially as scaffholding, for any sites provisioned afterward.
helm install erpnext --namespace erpnext frappe/erpnext --version 2.0.11 -f erpnext-values.yaml --wait
- Declare the MariaDB user account password secret for our upcoming job
kubectl apply --namespace erpnext -f erpnext-db-secret.yaml
- Run the 'create site' job and stream the job's pod until completion
kubectl apply --namespace erpnext -f create-site-job.yaml && kubectl logs --namespace erpnext job/create-erp-site
A successful completion will look something like:
> kubectl apply --namespace erpnext -f create-site-job.yaml && kubectl logs --namespace erpnext job/create-erp-site Attempt 1 to connect to mariadb.mariadb.svc.cluster.local:3306 Attempt 1 to connect to erpnext-redis-queue:12000 Attempt 1 to connect to erpnext-redis-cache:13000 Attempt 1 to connect to erpnext-redis-socketio:11000 Connections OK Created user _334389048b872a53 Created database _334389048b872a53 Granted privileges to user _334389048b872a53 and database _334389048b872a53 Starting database import... Imported from database /home/frappe/frappe-bench/apps/frappe/frappe/database/mariadb/framework_mariadb.sql Installing frappe... Updating DocTypes for frappe : [========================================] Updating country info : [========================================] Installing erpnext... Updating DocTypes for erpnext : [========================================] Updating customizations for Address *** Scheduler is disabled ***
Now your ERPNext instance is operational, and you have a site setup. The final step is to declare the ingress, so we can route our ERPNext site's name to the ERPNext service.
kubectl apply -f site-ingress.yaml
kubectl port-forward --namespace kube-system svc/traefik 8080:80
Now visit http://localhost:8080 and you should be prompted with the ERPNext login page.