Documentation / mysql
Multi-database schema
The chart supports declaring multiple databases — each with its own user,
password (auto-generated and Secret-managed), grants, and backup CronJob —
via the mysql.databases[] list in values.
This is the preferred schema for new deployments hosting more than one
logical database on a single MySQL release. Legacy single-database fields
(mysql.database / mysql.user / mysql.password) and mysql.extraDatabases[]
are still fully supported and merged in alongside.
Schema
mysql:
## Legacy single DB (still works; merged into unified list).
database: ""
user: ""
password: ""
## Legacy bare-name list (still works; merged into unified list).
extraDatabases: []
## Modern schema.
databases:
- name: tenant_one
user: t1_user
password: "" # auto-generated if empty
# existingSecret: "" # alternative — must have key "password"
# grants: # optional; default = ALL on this DB
# - "SELECT, INSERT ON `tenant_one`.* TO 't1_user'@'%'"
backup:
enabled: true # defaults to global backup.enabled
schedule: "0 3 * * *" # defaults to global backup.schedule
- name: tenant_two
user: t2_ro
grants:
- "SELECT ON `tenant_two`.* TO 't2_ro'@'%'"
- name: shared_logs # DB only, no user, no per-DB backup override
How the unified list is built
The mysql.unifiedDatabases helper (_helpers.tpl) merges three sources
into a single list, in this order:
- Legacy
mysql.database— one entry, markedisLegacy: true. Password handling stays at the existinguser-passwordSecret key. User creation is left to docker-entrypoint’sMYSQL_USER/MYSQL_PASSWORDenv vars (so legacy 00-users.sql skips it). mysql.extraDatabases[]— bare names, markedisExtra: true. Created asCREATE DATABASE IF NOT EXISTS. No user.mysql.databases[]— full schema. Each entry gets an injectedpasswordKey: db-password-N(Secret key name) andenvVar: MYSQL_DB_PASSWORD_N.
Consumers in the chart (statefulset.yaml, backup-cronjob.yaml,
secret.yaml) iterate this unified list with fromYamlArray.
Per-database backup CronJobs
When backup.enabled: true globally, the chart renders one CronJob per
database in the unified list:
<release>-mysql-backup-tenant_one schedule: 0 3 * * *
<release>-mysql-backup-tenant_two schedule: 0 2 * * * (global default)
<release>-mysql-backup-shared_logs schedule: 0 2 * * *
<release>-mysql-backup-legacydb schedule: 0 2 * * *
Each CronJob runs the db-backup image with DBNAME=<that db>. The image
uses DBNAME as both the mysqldump target AND the S3 prefix, so backups land at:
s3://<bucket>/<db>/<db>-DD-Mon-YYYY-HH-MM.sql.tar.gz
Per-DB overrides:
| Field | Default | Effect |
|---|---|---|
databases[i].backup.enabled |
true |
Set false to skip this DB’s CronJob entirely. |
databases[i].backup.schedule |
global backup.schedule |
Override cron for this DB. |
If the unified list is empty (no mysql.database, no extraDatabases, no
databases[]), the chart falls back to a single CronJob with DBNAME="all",
as it did before it supported several databases.
Per-database credentials
Each databases[] entry with user set gets a fresh auto-generated 24-char
password (unless password or existingSecret is supplied). Passwords are
stored in the chart’s main Secret under keys db-password-0, db-password-1,
… by list position. Helm’s lookup preserves them across upgrades.
If you supply existingSecret, the chart pulls the password from there
(expects key password) — useful for SealedSecrets or external secret
management.
Pull a generated password manually:
NAME=<release>-mysql
NS=<namespace>
# 0-indexed by databases[] position
kubectl get secret -n $NS $NAME -o jsonpath='{.data.db-password-0}' | base64 -d
Migration guidance
From legacy single-DB
You can keep mysql.database: "x" indefinitely — it continues to work and
gets its own backup CronJob. No migration required.
From extraDatabases[] to databases[]
If you’re using extraDatabases: ["analytics", "reports"] and want a
dedicated user + per-DB backup retention, move them to databases[]:
mysql:
# before:
# extraDatabases: ["analytics", "reports"]
# after:
databases:
- name: analytics
user: analytics_app
backup:
schedule: "0 1 * * *"
- name: reports
backup:
enabled: false # archived database, no backups
This is non-destructive on helm upgrade:
- The database already exists;
CREATE DATABASE IF NOT EXISTSis a no-op. - The new user gets created on first boot via 00-users.sql.
- The per-DB backup CronJob starts running on its configured schedule.
Caveat: the analytics_app user is created only when docker-entrypoint
runs /docker-entrypoint-initdb.d/00-users.sql, which only happens on a
fresh data dir. For existing deployments you must create the user manually
(same pattern as the clone_user upgrade step in upgrading-from-pre-clone.md).
Cross-database transactions and PITR
A heads-up if you use databases[] AND rely on PITR restore (docs/pitr-restore.md):
The PITR restore script uses mysqlbinlog --database=<RESTORE_DB> to filter
events. Cross-database transactions (e.g. INSERT INTO other_db.t ... issued
while connected to <RESTORE_DB>) are SKIPPED on replay. If you write
across databases on a multi-DB release, restore the WHOLE server instead:
spin up a fresh staging release with mysql.database unset, restore with
--database dropped (a follow-up to #809 may add an allDatabases: true
mode for the restore Job).