Bahriya Tersane

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:

  1. Legacy mysql.database — one entry, marked isLegacy: true. Password handling stays at the existing user-password Secret key. User creation is left to docker-entrypoint’s MYSQL_USER/MYSQL_PASSWORD env vars (so legacy 00-users.sql skips it).
  2. mysql.extraDatabases[] — bare names, marked isExtra: true. Created as CREATE DATABASE IF NOT EXISTS. No user.
  3. mysql.databases[] — full schema. Each entry gets an injected passwordKey: db-password-N (Secret key name) and envVar: 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 EXISTS is 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).