Skip to content

Instantly share code, notes, and snippets.

@robsonfelix
Last active June 30, 2025 18:31
Show Gist options
  • Select an option

  • Save robsonfelix/7874412edb008e0b121c3d783862213e to your computer and use it in GitHub Desktop.

Select an option

Save robsonfelix/7874412edb008e0b121c3d783862213e to your computer and use it in GitHub Desktop.
CiviCRM

Deploying CiviCRM on Kubernetes with a Custom Helm Chart This document provides a complete, self-contained guide and the necessary source files to create a Helm chart for deploying CiviCRM on a Kubernetes cluster. As there is no official or actively maintained community Helm chart for CiviCRM, this chart has been crafted to provide a reliable starting point. Introduction This Helm chart simplifies the deployment of CiviCRM by packaging the application and its dependencies into a single, manageable unit. It is designed with production-like considerations, including persistent storage, configurable services, and management of sensitive information. Core Features:

  • CiviCRM Application Deployment: Manages the CiviCRM application pods.
  • Database Dependency: Includes a dependency on the stable Bitnami MariaDB chart for the required database.
  • Service Exposure: Configurable internal Service and optional external Ingress.
  • Persistent Storage: Ensures data persistence for both the application and the database.
  • Configuration Management: Separates configuration and sensitive secrets from the application image. Downloadable Chart Generator Script To make getting started easier, you can use the shell script below. Copy this script, save it as create_chart.sh, make it executable with chmod +x create_chart.sh, and then run it with ./create_chart.sh. It will create the entire Helm chart directory and all the files described in this document for you. #!/bin/bash

This script creates a complete directory structure and all the necessary files

for a CiviCRM Helm chart. Run this script in the directory where you want

the 'civicrm' chart folder to be created.

set -e

CHART_DIR="civicrm"

--- Main function to create the chart ---

create_civicrm_chart() { echo "Creating CiviCRM Helm chart directory structure..." mkdir -p "${CHART_DIR}/templates" echo "Directory structure created." echo ""

--- Create Chart.yaml ---

echo "Creating ${CHART_DIR}/Chart.yaml..." cat <<'EOF' > "${CHART_DIR}/Chart.yaml" apiVersion: v2 name: civicrm description: A Helm chart for deploying CiviCRM on Kubernetes type: application version: 0.1.0 appVersion: "5.73"

dependencies:

  • name: mariadb version: "11.x.x" repository: "https://charts.bitnami.com/bitnami" condition: mariadb.enabled EOF

    --- Create values.yaml ---

    echo "Creating ${CHART_DIR}/values.yaml..." cat <<'EOF' > "${CHART_DIR}/values.yaml"

Default values for the CiviCRM Helm chart.

This is a YAML-formatted file.

Declare variables to be passed into your templates.

replicaCount: 1

image: repository: michaelmcandrew/civicrm-docker pullPolicy: IfNotPresent

Overrides the image tag whose default is the chart's appVersion.

tag: "5.73-wordpress"

imagePullSecrets: [] nameOverride: "" fullnameOverride: ""

serviceAccount:

Specifies whether a service account should be created

create: true

Annotations to add to the service account

annotations: {}

The name of the service account to use.

If not set and create is true, a name is generated using the fullname template

name: ""

podAnnotations: {}

podSecurityContext: {}

fsGroup: 2000

securityContext: {}

capabilities:

drop:

- ALL

readOnlyRootFilesystem: true

runAsNonRoot: true

runAsUser: 1000

service: type: ClusterIP port: 80

ingress: enabled: false className: "" annotations: {}

kubernetes.io/ingress.class: nginx

kubernetes.io/tls-acme: "true"

hosts:

  • host: civicrm.local paths:
    • path: / pathType: ImplementationSpecific tls: []

- secretName: civicrm-tls

hosts:

- civicrm.local

resources: {}

We usually recommend not to specify default resources and to leave this as a conscious

choice for the user. This also increases chances charts run on environments with little

resources, such as Minikube. If you do want to specify resources, uncomment the following

lines, adjust them as necessary, and remove the curly braces after 'resources:'.

limits:

cpu: 100m

memory: 128Mi

requests:

cpu: 100m

memory: 128Mi

autoscaling: enabled: false minReplicas: 1 maxReplicas: 10 targetCPUUtilizationPercentage: 80

targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity: {}

MariaDB sub-chart values

mariadb: enabled: true auth: rootPassword: "your-strong-root-password" database: "civicrm" username: "civicrm" password: "your-strong-civicrm-password" primary: persistence: enabled: true size: 8Gi

CiviCRM specific configuration

civicrm: site:

IMPORTANT: Set this to the hostname you will use to access CiviCRM

url: "http://civicrm.local" mail:

Configure your outbound mail settings

fromEmail: "[email protected]" EOF

--- Create .helmignore ---

echo "Creating ${CHART_DIR}/.helmignore..." cat <<'EOF' > "${CHART_DIR}/.helmignore"

Patterns to ignore when packaging Helm charts.

This prevents unwanted files and directories from being included in the final

chart package.

.DS_Store

Common VCS directories

.git/ .gitignore .bzr/ .bzrignore .hg/ .hgignore .svn/

Common backup files

*.swp *.bak *.tmp *.orig *~

Various IDEs

.project .idea/ *.tmproj .vscode/ EOF

--- Create templates/_helpers.tpl ---

echo "Creating ${CHART_DIR}/templates/_helpers.tpl..." cat <<'EOF' > "${CHART_DIR}/templates/_helpers.tpl" {{/* Expand the name of the chart. */}} {{- define "civicrm.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }}

{{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "civicrm.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }}

{{/* Create chart name and version as used by the chart label. */}} {{- define "civicrm.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }}

{{/* Common labels */}} {{- define "civicrm.labels" -}} helm.sh/chart: {{ include "civicrm.chart" . }} {{ include "civicrm.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }}

{{/* Selector labels */}} {{- define "civicrm.selectorLabels" -}} app.kubernetes.io/name: {{ include "civicrm.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }}

{{/* Create the name of the service account to use */}} {{- define "civicrm.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "civicrm.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} EOF

--- Create templates/deployment.yaml ---

echo "Creating ${CHART_DIR}/templates/deployment.yaml..." cat <<'EOF' > "${CHART_DIR}/templates/deployment.yaml" apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "civicrm.fullname" . }} labels: {{- include "civicrm.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: {{- include "civicrm.selectorLabels" . | nindent 6 }} template: metadata: {{- with .Values.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "civicrm.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "civicrm.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http containerPort: 80 protocol: TCP envFrom: - configMapRef: name: {{ include "civicrm.fullname" . }}-config - secretRef: name: {{ include "civicrm.fullname" . }}-secrets resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} EOF

--- Create templates/service.yaml ---

echo "Creating ${CHART_DIR}/templates/service.yaml..." cat <<'EOF' > "${CHART_DIR}/templates/service.yaml" apiVersion: v1 kind: Service metadata: name: {{ include "civicrm.fullname" . }} labels: {{- include "civicrm.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports:

  • port: {{ .Values.service.port }} targetPort: http protocol: TCP name: http selector: {{- include "civicrm.selectorLabels" . | nindent 4 }} EOF

--- Create templates/ingress.yaml ---

echo "Creating ${CHART_DIR}/templates/ingress.yaml..." cat <<'EOF' > "${CHART_DIR}/templates/ingress.yaml" {{- if .Values.ingress.enabled -}} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ include "civicrm.fullname" . }} labels: {{- include "civicrm.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: {{- if .Values.ingress.className }} ingressClassName: {{ .Values.ingress.className }} {{- end }} {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }}

  • hosts: {{- range .hosts }}
    • {{ . | quote }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: {{- range .Values.ingress.hosts }}
  • host: {{ .host | quote }} http: paths: {{- range .paths }} - path: {{ .path }} pathType: {{ .pathType }} backend: service: name: {{ include "civicrm.fullname" $ }} port: name: http {{- end }} {{- end }} {{- end }} EOF

--- Create templates/configmap.yaml ---

echo "Creating ${CHART_DIR}/templates/configmap.yaml..." cat <<'EOF' > "${CHART_DIR}/templates/configmap.yaml" apiVersion: v1 kind: ConfigMap metadata: name: {{ include "civicrm.fullname" . }}-config labels: {{- include "civicrm.labels" . | nindent 4 }} data: CIVICRM_DB_HOST: "{{ .Release.Name }}-mariadb" CIVICRM_DB_NAME: "{{ .Values.mariadb.auth.database }}" CIVICRM_SITE_URL: "{{ .Values.civicrm.site.url }}" CIVICRM_MAIL_FROM: "{{ .Values.civicrm.mail.fromEmail }}" EOF

--- Create templates/secrets.yaml ---

echo "Creating ${CHART_DIR}/templates/secrets.yaml..." cat <<'EOF' > "${CHART_DIR}/templates/secrets.yaml" apiVersion: v1 kind: Secret metadata: name: {{ include "civicrm.fullname" . }}-secrets labels: {{- include "civicrm.labels" . | nindent 4 }} type: Opaque stringData: CIVICRM_DB_USER: {{ .Values.mariadb.auth.username | quote }} CIVICRM_DB_PASS: {{ .Values.mariadb.auth.password | quote }} EOF

echo "" echo "CiviCRM Helm chart successfully created in '${CHART_DIR}/' directory." echo "Next steps:" echo "1. Customize '${CHART_DIR}/values.yaml' with your specific settings (passwords, URLs, etc.)." echo "2. Run 'helm dependency update ${CHART_DIR}' to fetch the MariaDB dependency." echo "3. Install the chart using 'helm install ${CHART_DIR}'." }

--- Run the main function ---

create_civicrm_chart

Chart File Structure Your Helm chart directory should be organized as follows. You can create these files and directories manually or use the script above. civicrm/ ├── Chart.yaml ├── values.yaml ├── .helmignore └── templates/ ├── _helpers.tpl ├── deployment.yaml ├── service.yaml ├── ingress.yaml ├── configmap.yaml └── secrets.yaml

Helm Chart Files Below is the complete source code for each file in the chart. Chart.yaml This file contains the chart's metadata and declares its dependency on the MariaDB sub-chart. apiVersion: v2 name: civicrm description: A Helm chart for deploying CiviCRM on Kubernetes type: application

This is the chart version. This version number should be incremented each time you make changes

to the chart and its templates.

version: 0.1.0

This is the version number of the application being deployed. This version is used by default

in the templates to reference the image tag.

appVersion: "5.73"

dependencies:

values.yaml This is the main configuration file. Customize your deployment by modifying these values.

Default values for the CiviCRM Helm chart.

This is a YAML-formatted file.

Declare variables to be passed into your templates.

replicaCount: 1

image: repository: michaelmcandrew/civicrm-docker pullPolicy: IfNotPresent

Overrides the image tag whose default is the chart's appVersion.

tag: "5.73-wordpress"

imagePullSecrets: [] nameOverride: "" fullnameOverride: ""

serviceAccount:

Specifies whether a service account should be created

create: true

Annotations to add to the service account

annotations: {}

The name of the service account to use.

If not set and create is true, a name is generated using the fullname template

name: ""

podAnnotations: {}

podSecurityContext: {}

fsGroup: 2000

securityContext: {}

capabilities:

drop:

- ALL

readOnlyRootFilesystem: true

runAsNonRoot: true

runAsUser: 1000

service: type: ClusterIP port: 80

ingress: enabled: false className: "" annotations: {}

kubernetes.io/ingress.class: nginx

kubernetes.io/tls-acme: "true"

hosts:

  • host: civicrm.local paths:
    • path: / pathType: ImplementationSpecific tls: []

- secretName: civicrm-tls

hosts:

- civicrm.local

resources: {}

We usually recommend not to specify default resources and to leave this as a conscious

choice for the user. This also increases chances charts run on environments with little

resources, such as Minikube. If you do want to specify resources, uncomment the following

lines, adjust them as necessary, and remove the curly braces after 'resources:'.

limits:

cpu: 100m

memory: 128Mi

requests:

cpu: 100m

memory: 128Mi

autoscaling: enabled: false minReplicas: 1 maxReplicas: 10 targetCPUUtilizationPercentage: 80

targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity: {}

MariaDB sub-chart values

mariadb: enabled: true auth: rootPassword: "your-strong-root-password" database: "civicrm" username: "civicrm" password: "your-strong-civicrm-password" primary: persistence: enabled: true size: 8Gi

CiviCRM specific configuration

civicrm: site:

IMPORTANT: Set this to the hostname you will use to access CiviCRM

url: "http://civicrm.local" mail:

Configure your outbound mail settings

fromEmail: "[email protected]"

.helmignore This file specifies patterns to ignore when packaging the chart.

Patterns to ignore when packaging Helm charts.

This prevents unwanted files and directories from being included in the final

chart package.

.DS_Store

Common VCS directories

.git/ .gitignore .bzr/ .bzrignore .hg/ .hgignore .svn/

Common backup files

*.swp *.bak *.tmp *.orig *~

Various IDEs

.project .idea/ *.tmproj .vscode/

templates/_helpers.tpl This file contains template helpers that can be reused throughout the chart. {{/* Expand the name of the chart. */}} {{- define "civicrm.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }}

{{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "civicrm.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }}

{{/* Create chart name and version as used by the chart label. */}} {{- define "civicrm.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }}

{{/* Common labels */}} {{- define "civicrm.labels" -}} helm.sh/chart: {{ include "civicrm.chart" . }} {{ include "civicrm.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }}

{{/* Selector labels */}} {{- define "civicrm.selectorLabels" -}} app.kubernetes.io/name: {{ include "civicrm.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }}

{{/* Create the name of the service account to use */}} {{- define "civicrm.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "civicrm.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }}

templates/deployment.yaml This template defines the Kubernetes Deployment for the CiviCRM application pods. apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "civicrm.fullname" . }} labels: {{- include "civicrm.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: {{- include "civicrm.selectorLabels" . | nindent 6 }} template: metadata: {{- with .Values.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "civicrm.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "civicrm.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http containerPort: 80 protocol: TCP envFrom: - configMapRef: name: {{ include "civicrm.fullname" . }}-config - secretRef: name: {{ include "civicrm.fullname" . }}-secrets resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }}

templates/service.yaml This template creates the Service that exposes the CiviCRM pods internally. apiVersion: v1 kind: Service metadata: name: {{ include "civicrm.fullname" . }} labels: {{- include "civicrm.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports:

  • port: {{ .Values.service.port }} targetPort: http protocol: TCP name: http selector: {{- include "civicrm.selectorLabels" . | nindent 4 }}

templates/ingress.yaml This template defines an Ingress resource for exposing CiviCRM externally. It is disabled by default. {{- if .Values.ingress.enabled -}} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ include "civicrm.fullname" . }} labels: {{- include "civicrm.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: {{- if .Values.ingress.className }} ingressClassName: {{ .Values.ingress.className }} {{- end }} {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }}

  • hosts: {{- range .hosts }}
    • {{ . | quote }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: {{- range .Values.ingress.hosts }}
  • host: {{ .host | quote }} http: paths: {{- range .paths }} - path: {{ .path }} pathType: {{ .pathType }} backend: service: name: {{ include "civicrm.fullname" $ }} port: name: http {{- end }} {{- end }} {{- end }}

templates/configmap.yaml This ConfigMap stores non-sensitive configuration data required by CiviCRM. apiVersion: v1 kind: ConfigMap metadata: name: {{ include "civicrm.fullname" . }}-config labels: {{- include "civicrm.labels" . | nindent 4 }} data: CIVICRM_DB_HOST: "{{ .Release.Name }}-mariadb" CIVICRM_DB_NAME: "{{ .Values.mariadb.auth.database }}" CIVICRM_SITE_URL: "{{ .Values.civicrm.site.url }}" CIVICRM_MAIL_FROM: "{{ .Values.civicrm.mail.fromEmail }}"

templates/secrets.yaml This template creates a Secret to securely store sensitive data like database credentials. apiVersion: v1 kind: Secret metadata: name: {{ include "civicrm.fullname" . }}-secrets labels: {{- include "civicrm.labels" . | nindent 4 }} type: Opaque stringData: CIVICRM_DB_USER: {{ .Values.mariadb.auth.username | quote }} CIVICRM_DB_PASS: {{ .Values.mariadb.auth.password | quote }}

Deployment Instructions Follow these steps to deploy your CiviCRM instance using this Helm chart.

  1. Prerequisites
  • A running Kubernetes cluster.
  • kubectl configured to communicate with your cluster.
  • Helm 3 installed.
  1. Create and Customize the Chart

  2. Create the directory structure and files as outlined above (or use the generator script).

  3. Crucially, edit civicrm/values.yaml:

    • Change mariadb.auth.rootPassword and mariadb.auth.password to strong, unique passwords.
    • Set civicrm.site.url to the URL you will use to access CiviCRM (e.g., http://civicrm.my-org.com).
    • If you are using an Ingress controller, set ingress.enabled to true and configure the hosts section.
  4. Install the Chart

  5. Fetch Dependencies: Navigate to your chart's root directory (civicrm/..) and run the dependency update command. This will download the MariaDB chart into your civicrm/charts directory. helm dependency update ./civicrm

  6. Install: Deploy the chart to your Kubernetes cluster. It's recommended to deploy into a dedicated namespace.

Replace 'my-civicrm' with your desired release name

Replace 'civicrm-ns' with your desired namespace

helm install my-civicrm ./civicrm --namespace civicrm-ns --create-namespace -f ./civicrm/values.yaml

  1. Access CiviCRM After a few minutes, the pods should be running. You can check their status with kubectl get pods -n civicrm-ns. To access your new CiviCRM instance, you will either:
    • Use Port Forwarding (for testing): kubectl port-forward svc/my-civicrm 8080:80 -n civicrm-ns

Then open http://localhost:8080 in your browser.

  • Use an Ingress: If you enabled the Ingress, navigate to the host you configured in values.yaml (e.g., http://civicrm.my-org.com).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment