MySQL Helm Chart
A simple, standalone MySQL Helm chart
- Official MySQL Images: Uses
docker.io/mysql:8.4.2 - Standalone Mode: Simple single-instance deployment
- Persistent Storage: StatefulSet with PVC support (default 20Gi)
- Configurable: Customizable MySQL configuration
- Secure: Kubernetes secrets for passwords
- Production Ready: Resource limits, health checks, security contexts
- Database Initialization: Automated database and user creation (optional)
- Automated Backups: Scheduled backups to S3-compatible storage (optional)
- Restore Operations: One-time restore from S3-compatible storage (optional)
- Global Labels: Common labels support for all resources
- Kubernetes 1.19+
- Helm 3.0+
- PV provisioner support (if persistence is enabled)
Add the repository to your Helm configuration:
helm repo add mysql https://comet-ml.github.io/comet-mysql-helm/
helm repo updateThen install using the repository:
# Basic installation
helm install my-mysql mysql/mysql
# With custom values
helm install my-mysql mysql/mysql \
--set auth.rootPassword=mypassword \
--set auth.database=mydb \
--set primary.persistence.size=50Gi
# Using a values file
helm install my-mysql mysql/mysql -f my-values.yamlClone the repository and install directly:
git clone https://github.com/comet-ml/comet-mysql-helm.git
cd comet-mysql-helm
helm install my-mysql ./To use this chart as a dependency in another Helm chart, add it to your Chart.yaml:
dependencies:
- name: mysql
version: "1.0.6"
repository: "https://comet-ml.github.io/comet-mysql-helm/"
condition: mysql.enabledThen run:
helm repo add mysql https://comet-ml.github.io/comet-mysql-helm/
helm dependency update
helm install my-app .See the examples/ directory for complete example configurations:
- Basic:
simple-values.yaml - Persistence:
existing-pvc-values.yaml - Configuration:
extra-args-values.yaml(command-line arguments) - Initialization:
initdb-scripts-values.yaml(SQL scripts),init-job-mixed-values.yaml,init-job-secretref-values.yaml - Availability:
pdb-values.yaml(Pod Disruption Budget) - Security:
network-policy-values.yaml(Network Policy),tls-values.yaml(TLS/SSL) - Backups:
backup-aws-s3-values.yaml,backup-aws-iam-role-values.yaml,backup-minio-values.yaml - Restore:
restore-aws-s3-values.yaml,restore-aws-iam-role-values.yaml,restore-minio-values.yaml - Complete Setup:
complete-setup-values.yaml - Labels:
common-labels-values.yaml
The chart supports two methods for database initialization:
SQL scripts that run automatically on first initialization (Bitnami-style):
initdbScripts:
createdb.sql: |-
CREATE DATABASE IF NOT EXISTS myapp
DEFAULT CHARACTER SET utf8
DEFAULT COLLATE utf8_general_ci;
CREATE USER IF NOT EXISTS 'myapp'@'%' IDENTIFIED BY 'myapp_password';
GRANT ALL ON `myapp`.* TO 'myapp'@'%';
FLUSH PRIVILEGES;Characteristics:
- Runs automatically on first initialization only
- Scripts execute in alphabetical order
- Mounted to
/docker-entrypoint-initdb.d(MySQL standard) - Only runs when data directory is empty
Flexible init job that runs after MySQL is ready:
initJob:
enabled: true
databases:
- name: myapp
username: myapp
password: myapp_password # or use passwordSecretRef
- name: production
username: produser
passwordSecretRef:
secretName: prod-db-secret
secretKey: passwordCharacteristics:
- Runs as Helm post-install/post-upgrade hook
- Supports both plain text passwords and secret references
- Runs every time (can be idempotent with IF NOT EXISTS)
- More flexible for dynamic configurations
You can use both methods together - initdbScripts run first, then initJob runs after.
initJob:
enabled: true
databases:
- name: myapp
username: myapp
password: myapp_password # or use passwordSecretRef
- name: production
username: produser
passwordSecretRef:
secretName: prod-db-secret
secretKey: passwordYou can pass additional command-line arguments to MySQL in two ways:
The extraFlags property automatically converts command-line flags to MySQL configuration format and appends them to the existing configuration (default or custom):
primary:
extraFlags: "--max-connections=500 --max-allowed-packet=64M --log-bin-trust-function-creators=1 --thread-stack=256K"Benefits:
- Flags are automatically converted to
my.cnfformat - Appends to the default configuration (or your custom
primary.configuration)
You can provide a complete MySQL configuration that replaces the default:
primary:
configuration: |-
[mysqld]
authentication_policy='* ,,'
skip-name-resolve
port=3306
datadir=/var/lib/mysql
max-connections=500
max-allowed-packet=64M
log-bin-trust-function-creators=1
thread-stack=256KUse cases:
- Complete control over the MySQL configuration
- When you need to override the entire default configuration
- Custom configurations that differ significantly from defaults
Note:
primary.extraFlagsappends flags to the existing configuration (default or custom)primary.configurationcompletely replaces the default configuration- You can use both together:
primary.configurationprovides the base config, andprimary.extraFlagsadds additional flags on top
See examples/extra-args-values.yaml for a complete example.
Schedule regular backups to S3-compatible storage (AWS S3, MinIO, etc.):
backup:
enabled: true
schedule: "0 2 * * *" # Daily at 2 AM
storage:
bucket: "my-backups"
region: "us-east-1"
prefix: "mysql-backups"
endpoint: "" # Empty for AWS S3, or "http://minio:9000" for MinIO
existingSecret: "aws-s3-credentials" # Optional - use IAM role if empty
retention: 7
databases: [] # Empty = all databasesOne-time restore operation:
restore:
enabled: true
backupFile: "mysql_backup_20250130_020000.sql.gz"
storage:
bucket: "my-backups"
region: "us-east-1"
prefix: "mysql-backups"
existingSecret: "aws-s3-credentials"Protect MySQL from voluntary disruptions (node drains, pod evictions):
podDisruptionBudget:
enabled: true
minAvailable: 1 # Keep at least 1 pod available (for single replica)
# OR
# maxUnavailable: 0 # Prevent any disruptionRestrict network traffic to/from MySQL pods:
networkPolicy:
enabled: true
allowExternal: true # Allow all pods in namespace
# OR restrict to specific namespaces/pods:
# allowExternal: false
# allowedNamespaces:
# - matchLabels:
# name: production
# allowedPods:
# - matchLabels:
# app: myappmysql -h <release-name>-mysql -u root -pkubectl port-forward svc/<release-name>-mysql 3306:3306
mysql -h 127.0.0.1 -P 3306 -u root -phelm repo update
helm upgrade my-mysql mysql/mysql -f values.yamlhelm upgrade my-mysql ./ -f values.yamlWhen migrating from Bitnami MySQL chart to this custom MySQL chart:
- Set dataDir to Bitnami's path in your values:
primary:
dataDir: /bitnami/mysql/data- Delete the MySQL StatefulSet before upgrading (PVCs are preserved):
kubectl delete statefulset <release-name>-mysql --cascade=orphanThis allows Helm to recreate the StatefulSet with the new chart while reusing the existing PVC and data.
Note: The volume will be mounted at /bitnami/mysql (base folder), and MySQL will use /bitnami/mysql/data as its datadir, matching Bitnami's structure.
helm uninstall my-mysqlNote: This removes all Kubernetes components but does not delete PVCs by default. To delete PVCs:
kubectl delete pvc -l app.kubernetes.io/name=mysqlkubectl get pods -l app.kubernetes.io/name=mysql
kubectl describe pod <pod-name>
kubectl logs <pod-name>kubectl get pvc
kubectl describe pvc data-<release-name>-mysql-0kubectl exec -it <pod-name> -- /bin/bash┌─────────────────────────────────────┐
│ Service (ClusterIP) │
│ <release-name>-mysql:3306 │
└─────────────────┬───────────────────┘
│
┌─────────────────▼───────────────────┐
│ StatefulSet (Primary) │
│ <release-name>-mysql-0 │
│ - MySQL 8.4.2 (Official Image) │
│ - Port 3306 │
│ - Health Checks │
│ - Resource Limits │
└─────────────────┬───────────────────┘
│
┌─────────────────▼───────────────────┐
│ PersistentVolumeClaim │
│ data-<release-name>-mysql-0 │
│ - /var/lib/mysql │
│ - Size: 20Gi (default) │
│ - Storage Class: configurable │
└─────────────────────────────────────┘
| Key | Type | Default | Description |
|---|---|---|---|
| architecture.mode | string | "standalone" |
|
| auth.database | string | "my_database" |
|
| auth.existingSecret | string | "" |
|
| auth.password | string | "" |
|
| auth.rootPassword | string | "changeme" |
|
| auth.username | string | "" |
|
| backup.databases | list | [] |
|
| backup.enabled | bool | false |
|
| backup.nodeSelector | object | {} |
|
| backup.resources.limits.cpu | string | "500m" |
|
| backup.resources.limits.memory | string | "512Mi" |
|
| backup.resources.requests.cpu | string | "250m" |
|
| backup.resources.requests.memory | string | "256Mi" |
|
| backup.retention | int | 7 |
|
| backup.schedule | string | "0 2 * * *" |
|
| backup.serviceAccountName | string | "" |
|
| backup.storage.bucket | string | "" |
|
| backup.storage.endpoint | string | "" |
|
| backup.storage.existingSecret | string | "" |
|
| backup.storage.prefix | string | "mysql-backups" |
|
| backup.storage.region | string | "us-east-1" |
|
| backup.tolerations | list | [] |
|
| containerSecurityContext.enabled | bool | true |
|
| containerSecurityContext.runAsNonRoot | bool | false |
|
| containerSecurityContext.runAsUser | string | "" |
|
| fullnameOverride | string | "" |
|
| global.commonLabels | object | {} |
|
| global.imageRegistry | string | "" |
|
| global.storageClass | string | "" |
|
| image.pullPolicy | string | "IfNotPresent" |
|
| image.pullSecrets | list | [] |
|
| image.registry | string | "docker.io" |
|
| image.repository | string | "mysql" |
|
| image.tag | string | "8.4.2" |
|
| initJob.annotations."helm.sh/hook" | string | "post-install,post-upgrade" |
|
| initJob.annotations."helm.sh/hook-delete-policy" | string | "before-hook-creation,hook-succeeded" |
|
| initJob.annotations."helm.sh/hook-weight" | string | "5" |
|
| initJob.databases | list | [] |
|
| initJob.enabled | bool | false |
|
| initJob.resources.limits.cpu | string | "500m" |
|
| initJob.resources.limits.memory | string | "256Mi" |
|
| initJob.resources.requests.cpu | string | "100m" |
|
| initJob.resources.requests.memory | string | "128Mi" |
|
| initdbScripts | object | {} |
|
| nameOverride | string | "" |
|
| networkPolicy.allowExternal | bool | false |
|
| networkPolicy.allowedNamespaces | list | [] |
|
| networkPolicy.allowedPods | list | [] |
|
| networkPolicy.enabled | bool | false |
|
| networkPolicy.extraEgress | list | [] |
|
| networkPolicy.extraIngress | list | [] |
|
| podDisruptionBudget.enabled | bool | false |
|
| podDisruptionBudget.maxUnavailable | int | 1 |
|
| podDisruptionBudget.minAvailable | string | "" |
|
| podSecurityContext.enabled | bool | true |
|
| podSecurityContext.fsGroup | int | 999 |
|
| primary.affinity | object | {} |
|
| primary.configuration | string | "[mysqld]\nauthentication_policy='* ,,'\nskip-name-resolve\nexplicit_defaults_for_timestamp\nport=3306\ndatadir=/var/lib/mysql\nsocket=/var/run/mysqld/mysqld.sock\npid-file=/var/run/mysqld/mysqld.pid\nmax_allowed_packet=16M\nbind-address=0.0.0.0\ncharacter-set-server=utf8mb4\ncollation-server=utf8mb4_unicode_ci\nslow_query_log=0\nlong_query_time=10.0" |
|
| primary.dataDir | string | "/var/lib/mysql" |
|
| primary.existingConfigmap | string | "" |
|
| primary.extraEnvVars | list | [] |
|
| primary.extraFlags | string | "" |
|
| primary.nodeSelector | object | {} |
|
| primary.persistence.accessModes[0] | string | "ReadWriteOnce" |
|
| primary.persistence.annotations | object | {} |
|
| primary.persistence.enabled | bool | true |
|
| primary.persistence.existingClaim | string | "" |
|
| primary.persistence.selector | object | {} |
|
| primary.persistence.size | string | "20Gi" |
|
| primary.persistence.storageClass | string | "" |
|
| primary.podAnnotations | object | {} |
|
| primary.podLabels | object | {} |
|
| primary.resources.limits.cpu | string | "2000m" |
|
| primary.resources.limits.memory | string | "2Gi" |
|
| primary.resources.requests.cpu | string | "500m" |
|
| primary.resources.requests.memory | string | "512Mi" |
|
| primary.tolerations | list | [] |
|
| restore.backupFile | string | "" |
|
| restore.enabled | bool | false |
|
| restore.nodeSelector | object | {} |
|
| restore.resources.limits.cpu | string | "1000m" |
|
| restore.resources.limits.memory | string | "1Gi" |
|
| restore.resources.requests.cpu | string | "500m" |
|
| restore.resources.requests.memory | string | "512Mi" |
|
| restore.serviceAccountName | string | "" |
|
| restore.storage.bucket | string | "" |
|
| restore.storage.endpoint | string | "" |
|
| restore.storage.existingSecret | string | "" |
|
| restore.storage.prefix | string | "mysql-backups" |
|
| restore.storage.region | string | "us-east-1" |
|
| restore.tolerations | list | [] |
|
| service.annotations | object | {} |
|
| service.clusterIP | string | "" |
|
| service.loadBalancerIP | string | "" |
|
| service.loadBalancerSourceRanges | list | [] |
|
| service.nodePort | string | "" |
|
| service.port | int | 3306 |
|
| service.type | string | "ClusterIP" |
|
| serviceAccount.annotations | object | {} |
|
| serviceAccount.create | bool | true |
|
| serviceAccount.name | string | "" |
|
| tls.certCAFilename | string | "ca.crt" |
|
| tls.certFilename | string | "tls.crt" |
|
| tls.certKeyFilename | string | "tls.key" |
|
| tls.enabled | bool | false |
|
| tls.existingSecret | string | "" |
|
| tls.requireSecureTransport | bool | false |
This chart is provided as-is.
Autogenerated from chart metadata using helm-docs v1.14.2