MySQL
Deploy MySQL on Kubernetes using the official mysql image. The chart supports standalone and replication
architectures, S3-compatible backups, Prometheus metrics, TLS, dual-stack Services, External Secrets Operator, and
production hardening controls that can be enabled without changing the development-friendly defaults.
Key Features
- Standalone and source-replica - Single instance or source with read replicas.
- Official MySQL image - Uses
docker.io/library/mysql:9.7.0by default. - Deterministic credentials - Inline values for development or existing Secrets/External Secrets for production.
- TLS - Server TLS, optional secure transport requirement, internal client TLS, and private-key permission normalization.
- NetworkPolicy - Optional ingress rules plus explicit egress for DNS, same-namespace MySQL, HTTPS, and custom targets.
- S3 backup - Scheduled
mysqldumpCronJobs with existing Secret support for object storage credentials. - Observability -
mysqld-exportersidecar with optional ServiceMonitor. - Dual-stack Services - Optional
ipFamilyPolicyandipFamiliesacross chart-managed Services. - Least privilege - Non-root containers, dropped capabilities, and opt-in ServiceAccount token mounting.
Architecture
Standalone
Single MySQL pod with persistent storage, metrics, optional TLS, and optional backup CronJob.
Source-Replica
Writable source pod with binary log retention and read-only replicas behind a replica Service.
Installation
HTTPS repository:
helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install my-mysql helmforge/mysql -f values.yaml
OCI registry:
helm install my-mysql oci://ghcr.io/helmforgedev/helm/mysql -f values.yaml
The chart can run with generated credentials and small storage defaults, but production installs should pin credentials in an existing Secret or External Secrets backend, size PVCs/resources explicitly, and enable backup before storing critical data.
Deployment Examples
# values.yaml - standalone development or small workload
architecture: standalone
auth:
rootPassword: 'my-secret-password'
database: myapp
username: myuser
password: 'user-password'
standalone:
persistence:
size: 20Gi
metrics:
enabled: true
serviceMonitor:
enabled: true# values.yaml - source with two read replicas
architecture: replication
auth:
rootPassword: 'my-secret-password'
database: myapp
username: myuser
password: 'user-password'
replicationPassword: 'repl-password'
replication:
source:
persistence:
size: 20Gi
readReplicas:
replicaCount: 2
persistence:
size: 20Gi# values.yaml - backup credentials from an existing Secret
architecture: standalone
auth:
existingSecret: mysql-auth
database: myapp
username: myapp
standalone:
persistence:
size: 20Gi
backup:
enabled: true
schedule: '0 3 * * *'
s3:
endpoint: https://s3.amazonaws.com
bucket: my-mysql-backups
prefix: mysql
existingSecret: mysql-backup# values.yaml - production baseline with replication, TLS, backup, and policies
architecture: replication
auth:
existingSecret: mysql-auth
database: myapp
username: myapp
replication:
source:
persistence:
size: 100Gi
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: '2'
memory: 2Gi
readReplicas:
replicaCount: 2
persistence:
size: 100Gi
resources:
requests:
cpu: 250m
memory: 768Mi
limits:
cpu: '1'
memory: 1536Mi
tls:
enabled: true
existingSecret: mysql-tls
requireSecureTransport: true
volumePermissions:
enabled: true
client:
enabled: true
sslMode: REQUIRED
metrics:
enabled: true
serviceMonitor:
enabled: true
networkPolicy:
enabled: true
egress:
enabled: true
allowDNS: true
allowSameNamespaceMySQL: true
allowHTTPS: true
serviceAccount:
create: true
automountServiceAccountToken: false
backup:
enabled: true
s3:
endpoint: https://s3.example.com
bucket: mysql-backups
existingSecret: mysql-backup# values.yaml - render ExternalSecret resources for existing ESO clusters
auth:
existingSecret: mysql-auth
tls:
enabled: true
existingSecret: mysql-tls
backup:
enabled: true
s3:
existingSecret: mysql-backup
externalSecrets:
enabled: true
apiVersion: external-secrets.io/v1
secretStoreRef:
name: platform-secrets
kind: ClusterSecretStore
auth:
enabled: true
targetName: mysql-auth
rootPasswordRemoteRef:
key: prod/mysql
property: root-password
userPasswordRemoteRef:
key: prod/mysql
property: user-password
replicationPasswordRemoteRef:
key: prod/mysql
property: replication-password
tls:
enabled: true
targetName: mysql-tls
caRemoteRef:
key: prod/mysql-tls
property: ca.crt
certRemoteRef:
key: prod/mysql-tls
property: tls.crt
keyRemoteRef:
key: prod/mysql-tls
property: tls.key
backup:
enabled: true
targetName: mysql-backup
accessKeyRemoteRef:
key: prod/mysql-backup
property: access-key
secretKeyRemoteRef:
key: prod/mysql-backup
property: secret-keyThe chart does not install External Secrets Operator and does not create a SecretStore or ClusterSecretStore.
Enable this only when the operator and referenced store already exist.
Production Path
Keep defaults for local development, CI smoke tests, and disposable environments. For production, make these decisions explicitly:
- Store
auth.*Password, TLS material, and backup credentials in existing Secrets or External Secrets. - Size
replication.source.persistence,replication.readReplicas.persistence, and resources for the actual workload. - Enable
tls.requireSecureTransportwhen clients should be forced to use encrypted TCP connections. - Enable metrics and backup before accepting durable workload traffic.
- Enable NetworkPolicy only after modelling required ingress and egress paths.
TLS private key permissions
Some MySQL versions reject private keys projected from Secrets when mode or ownership is too permissive for the runtime
user. tls.volumePermissions.enabled copies TLS material into an emptyDir, normalizes ownership for UID/GID 999, and
restricts the private key before mysqld starts.
NetworkPolicy egress
networkPolicy.egress.enabled=false preserves default CNI egress behavior. When enabled, use the structured switches for
DNS, same-namespace MySQL replication/internal clients, HTTPS to object storage or external secret backends, and
networkPolicy.egress.extraTo for platform-specific destinations.
ServiceAccount token mount
serviceAccount.automountServiceAccountToken defaults to false. Keep it disabled unless a sidecar, extension, or
platform integration inside the pod needs Kubernetes API credentials.
Configuration Reference
Core and Image
| Parameter | Type | Default | Description |
|---|---|---|---|
architecture | string | standalone | Deployment architecture: standalone or replication. |
image.repository | string | docker.io/library/mysql | MySQL runtime image repository. |
image.tag | string | "9.7.0" | MySQL runtime image tag. |
clusterDomain | string | cluster.local | Kubernetes cluster DNS domain. |
commonLabels | object | {} | Extra labels added to all resources. |
Authentication
| Parameter | Type | Default | Description |
|---|---|---|---|
auth.rootPassword | string | "" | Root password when no existing Secret is used. |
auth.database | string | app | Application database created on first bootstrap. |
auth.username | string | app | Application user created on first bootstrap. |
auth.password | string | "" | Application user password when no existing Secret is used. |
auth.replicationUsername | string | replicator | Replication user used by read replicas. |
auth.replicationPassword | string | "" | Replication password when no existing Secret is used. |
auth.existingSecret | string | "" | Existing Secret containing MySQL passwords. |
auth.existingSecretRootPasswordKey | string | mysql-root-password | Secret key for the root password. |
auth.existingSecretUserPasswordKey | string | mysql-user-password | Secret key for the application user password. |
auth.existingSecretReplicationPasswordKey | string | mysql-replication-password | Secret key for the replication password. |
Replication, Persistence, and Backup
| Parameter | Type | Default | Description |
|---|---|---|---|
standalone.persistence.size | string | 8Gi | PVC size for standalone mode. |
replication.source.persistence.size | string | 20Gi | PVC size for the source pod. |
replication.readReplicas.replicaCount | integer | 2 | Number of read replica pods. |
replication.readReplicas.persistence.size | string | 20Gi | PVC size for each read replica. |
replication.binlog.retentionDays | integer | 7 | Binary log retention in whole days. |
replication.pdb.enabled | boolean | true | Enable a PDB by default in replication mode. |
backup.enabled | boolean | false | Enable the built-in backup CronJob. |
backup.schedule | string | "0 3 * * *" | Backup schedule. |
backup.s3.existingSecret | string | "" | Existing Secret with S3 access and secret keys. |
backup.database.mysqldumpArgs | string | See chart values | Extra arguments passed before --all-databases. |
TLS, Services, Metrics, and Policies
| Parameter | Type | Default | Description |
|---|---|---|---|
tls.enabled | boolean | false | Enable MySQL server TLS. |
tls.existingSecret | string | "" | Existing Secret containing TLS material. |
tls.requireSecureTransport | boolean | false | Require encrypted TCP client connections. |
tls.volumePermissions.enabled | boolean | false | Normalize copied TLS private-key ownership and permissions before MySQL starts. |
tls.client.enabled | boolean | false | Enable TLS for chart-managed internal TCP clients. |
service.ipFamilyPolicy | string | omitted | Service IP family policy: SingleStack, PreferDualStack, or RequireDualStack. Omit for cluster default. |
service.ipFamilies | array | omitted | Ordered list of IP families (IPv4, IPv6). Omit for cluster default. |
metrics.enabled | boolean | false | Enable mysqld-exporter. |
metrics.image.tag | string | "v0.17.2" | mysqld-exporter image tag. |
networkPolicy.enabled | boolean | false | Create a NetworkPolicy for MySQL pods. |
networkPolicy.egress.enabled | boolean | false | Add explicit egress rules to the NetworkPolicy. |
serviceAccount.automountServiceAccountToken | boolean | false | Mount Kubernetes API credentials into workload pods. |
External Secrets Operator
| Parameter | Type | Default | Description |
|---|---|---|---|
externalSecrets.enabled | boolean | false | Render ExternalSecret resources. |
externalSecrets.apiVersion | string | external-secrets.io/v1 | ExternalSecret API version. |
externalSecrets.secretStoreRef.name | string | "" | Existing SecretStore or ClusterSecretStore name. |
externalSecrets.auth.enabled | boolean | false | Manage the MySQL auth Secret through ESO. |
externalSecrets.tls.enabled | boolean | false | Manage the MySQL TLS Secret through ESO. |
externalSecrets.backup.enabled | boolean | false | Manage the backup object-storage Secret through ESO. |
Dual-stack Networking
By default, service.ipFamilyPolicy and service.ipFamilies are unset and the cluster default remains authoritative.
Set them only when your cluster supports the requested families.
service:
ipFamilyPolicy: PreferDualStack
ipFamilies:
- IPv4
- IPv6
Use PreferDualStack for mixed environments. RequireDualStack should be reserved for clusters where both IPv4 and
IPv6 are guaranteed.
Upgrade Notes
MySQL stores credentials during initial setup. On upgrade, ensure the provided Secret values match the existing data directory or the pod can fail authentication during startup.
- Switching from
standalonetoreplicationrequires a planned migration or re-initialization. - Backup CronJob changes take effect on the next scheduled run.
- Read replicas are read-only; write traffic must target the source/client Service.
Common Issues
The most common cause is credentials that do not match the existing data directory. Check pod logs and verify the Secret values before deleting PVCs.
Enable metrics and monitor replica lag. Persistent lag usually means replicas need more CPU, memory, storage IOPS, or a reduced write workload.