> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frankenpress.com/llms.txt
> Use this file to discover all available pages before exploring further.

# charts

> Helm chart for deploying FrankenPress sites to Kubernetes

[`charts`](https://github.com/frankenpress/charts) ships **`site`** —
a Helm chart that deploys a single FrankenPress WordPress site to
Kubernetes. Bitnami chart style throughout: every value annotated with
`## @param`, naming + labels via the `bitnami/common` library, optional
subchart deps via the Bitnami OCI registry.

## Install

The chart is published as an OCI artifact on GHCR:

```bash theme={null}
helm install mysite oci://ghcr.io/frankenpress/charts/site \
  --version 0.13.2 \
  --namespace mysite --create-namespace
```

For the kind-cluster quickstart, see [Quickstart](/quickstart).

For the production topology (DragonflyDB Operator, MariaDB Operator,
AWS S3, External Secrets), see [Operations → Production
topology](/operations/production).

## What gets deployed

```mermaid theme={null}
flowchart TD
    subgraph release["Per-release resources"]
        deploy["<b>Deployment</b><br/>Caddy + FrankenPHP<br/>:8080 http, :9145 metrics<br/><i>+ sync-admin-credentials initContainer</i>"]
        svc["<b>Service</b> (ClusterIP)"]
        ingress(["Ingress / HTTPRoute<br/>(opt-in)"])
        wpcron["<b>CronJob</b><br/>wp-cron (every 5 min)"]
        installJob["<b>Job</b> (post-install + post-upgrade hooks)<br/>idempotent wp core install + update-db"]
        keys["Secret: keys+salts<br/>(auto-generated)"]
        admin["Secret: admin install creds<br/>(auto-generated, or existingSecret)"]
    end

    subgraph subcharts["Bundled subcharts (kind dev only — NOT for production)"]
        mariadb[("MariaDB<br/>bitnami/mariadb")]
        redis[("Redis<br/>bitnami/redis")]
        minio[("MinIO + Console<br/>bitnami/minio")]
    end

    deploy --> svc --> ingress
    deploy -.reads.-> keys
    deploy -.reads.-> admin
    deploy -.reads.-> mariadb
    deploy -.reads.-> redis
    deploy -.reads.-> minio
    installJob -.reads.-> admin
    installJob -.reads.-> keys
    installJob -.installs WP into.-> mariadb
    wpcron -.reads.-> keys
    wpcron -.runs cron via.-> mariadb
```

The install Job is new in chart **v0.2.0** — fresh deploys produce a
usable site without manual `kubectl exec ... wp core install`. The
`sync-admin-credentials` initContainer (chart **v0.4.0+**) reconciles
`wp_users` from the install Secret on every Pod start. See
[First install](#first-install) for credentials and
[Operations → Admin credential rotation](/operations/admin-credential-rotation)
for the rotation flow.

## Subchart dependencies

| Subchart          | Default       | Purpose                                                                  |
| ----------------- | ------------- | ------------------------------------------------------------------------ |
| `bitnami/common`  | always loaded | Library helpers (fullname, labels, image templating)                     |
| `bitnami/mariadb` | enabled       | In-cluster DB for instant deploy. **Not for production.**                |
| `bitnami/redis`   | enabled       | Souin HTTP cache backend. **Production: swap to DragonflyDB Operator.**  |
| `bitnami/minio`   | enabled       | S3-compatible object storage. **Production: swap to AWS S3 / R2 / GCS.** |

<Warning>
  Bitnami withdrew their `bitnami/<name>` images from free public
  Docker Hub in 2025 ("Bitnami Secure Images" commercial pivot). The
  chart's default values point at the `bitnamilegacy/<name>` mirror
  Bitnami publishes for community use. We track this as a Renovate
  concern; see the values.yaml comments.
</Warning>

## Values reference

The full annotated values reference is in
[`charts/site/values.yaml`](https://github.com/frankenpress/charts/blob/main/charts/site/values.yaml);
the most-used keys:

| Key                                                               | Default                      | Purpose                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ----------------------------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `image.repository`                                                | `frankenpress/site-template` | Your site image                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `image.tag`                                                       | `""` (chart appVersion)      | Pin per release                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `site.url`                                                        | `http://site.localhost`      | `WP_HOME` — must match your access URL                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `site.env`                                                        | `production`                 | Selects `config/environments/<env>.php`                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `keysSalts.autoGenerate`                                          | `true`                       | Off → set `keysSalts.existingSecret`                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `siteInstall.enabled`                                             | `true`                       | Off → skip auto `wp core install` (use for DB-restore deploys)                                                                                                                                                                                                                                                                                                                                                                                              |
| `siteInstall.adminUser`                                           | `admin`                      | Auto-generated path                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `siteInstall.existingSecret`                                      | `""`                         | BYO Secret (key names configurable)                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `syncAdminCredentials`                                            | `true`                       | InitContainer that reconciles wp\_users from the install Secret on every Pod start (idempotent, multi-replica safe). Pair with [Reloader](/operations/email#auto-rolling-on-secret-change-reloader) for self-driving rotation. Set `false` to skip — see [Admin credential rotation](/operations/admin-credential-rotation).                                                                                                                                |
| `replicaCount`                                                    | `1`                          | Deployment replicas (HPA optional)                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `revisionHistoryLimit`                                            | `3`                          | Old ReplicaSets retained for `kubectl rollout undo`. Lower than the k8s default of 10 since `helm rollback` and image-tag-pinned re-deploys are the canonical rollback paths in this stack. Chart **v0.4.1+**.                                                                                                                                                                                                                                              |
| `ingress.enabled` / `httpRoute.enabled`                           | `false` / `false`            | Pick one for external routing                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `mariadb.enabled` / `redis.enabled` / `minio.enabled`             | `true` / `true` / `true`     | Bundled subcharts (kind dev)                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `externalDatabase.host`                                           | `""`                         | Used when `mariadb.enabled=false`                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `externalCache.host`                                              | `""`                         | Used when `redis.enabled=false`                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `externalS3.bucket`                                               | `site-media`                 | Used when `minio.enabled=false`                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `wpCron.enabled`                                                  | `true`                       | Run a CronJob that fires `wp cron event run --due-now`. When true, the chart also sets `DISABLE_WP_CRON=true` on the site so WP's in-process pseudo-cron doesn't fire alongside it (the two settings are coupled). Disable to fall back to WP's request-triggered cron.                                                                                                                                                                                     |
| `wpCron.schedule`                                                 | `*/5 * * * *`                | wp-cron CronJob frequency                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `serviceMonitor.enabled`                                          | `false`                      | Render a prometheus-operator `ServiceMonitor` for the `:9145/metrics` endpoint. Self-gates on the `monitoring.coreos.com/v1` CRD — leaving on for clusters without it is safe (renders nothing). Set `serviceMonitor.labels` to whatever your Prometheus's `serviceMonitorSelector` expects.                                                                                                                                                                |
| `smtp.enabled`                                                    | `false`                      | Wire `wp_mail()` through SMTP. Inject 5 `FP_SMTP_*` env vars + 2 `secretKeyRef` env entries into the Deployment, wpcron CronJob, and install Job. Pairs with the [SMTPMailer mu-plugin component](/components/mu-plugin#smtpmailer). Transport-agnostic — Postmark, SendGrid, Mailgun, AWS SES, in-cluster relay. See [Operations → Email](/operations/email) for provider recipes. Requires `site-template` v0.2.4+.                                       |
| `smtp.host` / `smtp.port` / `smtp.encryption`                     | `""` / `587` / `tls`         | SMTP server, TCP port, transport encryption (`tls` for STARTTLS, `ssl` for implicit TLS, `""` for plaintext local-dev only).                                                                                                                                                                                                                                                                                                                                |
| `smtp.auth.existingSecret`                                        | `""`                         | Required when `smtp.enabled=true` and the server requires auth. Secret keys default to `username` / `password`; override via `smtp.auth.usernameKey` / `passwordKey`. Leave empty for unauthenticated in-cluster relays.                                                                                                                                                                                                                                    |
| `tmpfsSize.tmp` / `data` / `config`                               | `256Mi` / `64Mi` / `16Mi`    | tmpfs sizes for the read-only-root pod                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `reloader.enabled` / `reloader.secretsToWatch`                    | `false` / `[]`               | First-class [Stakater Reloader](https://github.com/stakater/Reloader) integration. Set `enabled: true` and list Secret names to emit a `secret.reloader.stakater.com/reload: "<csv>"` annotation on the Deployment that auto-rolls Pods on Secret changes. Supersedes the older `commonAnnotations` pattern (which still works as a fallback). Requires Reloader installed cluster-wide; safe when it isn't (annotation becomes inert). Chart **v0.13.0+**. |
| `podDisruptionBudget.enabled` / `minAvailable` / `maxUnavailable` | `false` / `1` / `null`       | Render a `PodDisruptionBudget`. Recommended for `replicaCount >= 2` or `autoscaling.enabled=true` so node drains and cluster upgrades don't take all replicas at once. Disabled by default — a single-replica deploy can't satisfy a PDB without making drains hang. Chart **v0.13.0+**.                                                                                                                                                                    |
| `startupProbe.enabled`                                            | `true`                       | HTTP `GET /healthz` startup probe gating liveness/readiness. Protects first-boot pods (snapshot apply, runtime cold-start) from being killed by readiness failures. Default on; turn off only if you've ripped out the runtime's health endpoint. Chart **v0.13.0+**.                                                                                                                                                                                       |
| `snapshotExport.enabled`                                          | `false`                      | Wire `FP_SNAPSHOT_*` env vars into the site Deployment, activating the [SnapshotExporter mu-plugin component](/components/mu-plugin#snapshotexporter). On enablement, the component captures site state to S3 daily at site-local midnight + on an admin-button trigger; designers fetch the bundle locally with [`fp pull`](/designer-flow-fp-pull). Requires `mu-plugin` v0.15.0+. Chart **v0.14.0+**.                                                    |
| `snapshotExport.bucket`                                           | `""`                         | Snapshot bucket name (e.g. `sts-production-snapshots-eu-west-2-533158516642`). Provisioned out-of-band by `tg_frankenpress`. Required when `snapshotExport.enabled=true`.                                                                                                                                                                                                                                                                                   |
| `snapshotExport.region`                                           | `""`                         | AWS region. Falls back to `externalS3.region` when empty.                                                                                                                                                                                                                                                                                                                                                                                                   |
| `snapshotExport.existingSecret`                                   | `""`                         | K8s Secret with `access-key` + `secret-key` keys (typically ESO-projected from ASM `service_user/prod/<site>-production-snapshots`). Key names override via `existingSecretAccessKeyKey` / `existingSecretSecretKeyKey`.                                                                                                                                                                                                                                    |

A condensed reference is at [Operations → Configuration](/operations/configuration).

The chart deliberately doesn't ship a log shipper — pod streams are
backend-neutral and any cluster-side agent picks them up. Setup for
Vector / Grafana Alloy / Datadog Agent is in [Operations → Logging](/operations/logging).

## First install

The chart runs `wp core install` and `wp core update-db` from a
post-install/post-upgrade Helm hook Job — a fresh `helm install`
produces a usable WordPress site without any manual `kubectl exec`.

The install step is idempotent (`wp core is-installed` short-circuits
re-runs), so the Job is safe across `helm upgrade`s.

### Default behavior

By default, the chart creates a `<release>-site-install` Secret with
a random 32-char admin password. Retrieve it after install:

```bash theme={null}
kubectl --namespace mysite get secret mysite-install \
  -o jsonpath='{.data.admin_password}' | base64 -d
```

Override the username, email, or password at install:

```bash theme={null}
helm install mysite oci://ghcr.io/frankenpress/charts/site \
  --set siteInstall.adminUser=alice \
  --set siteInstall.adminEmail=alice@example.com \
  --set siteInstall.adminPassword=please-change-me
```

### Bring-your-own Secret

Point at a pre-existing Secret with the admin credentials:

```yaml theme={null}
siteInstall:
  existingSecret: mysite-admin
  existingSecretAdminUserKey: admin_user      # default
  existingSecretAdminEmailKey: admin_email    # default
  existingSecretAdminPasswordKey: admin_password  # default
```

<Note>
  The chart doesn't care how the Secret got there — `kubectl create secret`,
  External Secrets Operator (any provider: AWS Secrets Manager, GCP Secret
  Manager, Vault, 1Password), Sealed Secrets, SOPS-decrypted, anything
  works. The configurable key names let the Secret use whatever schema
  the source system produces.
</Note>

<Tip>
  Already running with auto-generated credentials and now want to move
  them into your secret manager? See
  [Production → Migrating auto-generated Secrets to a secret manager](/operations/production#migrating-auto-generated-secrets-to-a-secret-manager)
  for the `kubectl get secret -o json | jq` extract commands (also
  covers WP keys+salts and the DB / S3 Secrets).
</Tip>

### Password rotation

WordPress stores a *hash* of the admin password in `wp_users`, so simply
updating the Secret value won't change the live login. The chart's
`sync-admin-credentials` initContainer (default on) handles this on
every Pod start — pair it with
[Stakater Reloader](/operations/email#auto-rolling-on-secret-change-reloader)
so the Pod actually rolls when the Secret changes:

```yaml theme={null}
# values.yaml
syncAdminCredentials: true   # default — initContainer on every site Pod

# Chart v0.13.0+ ships a typed Reloader block. The chart emits the
# correct `secret.reloader.stakater.com/reload` annotation on the
# Deployment (where Reloader actually looks) — no need to remember the
# annotation name or the commonAnnotations vs. podAnnotations gotcha.
reloader:
  enabled: true
  secretsToWatch:
    - <release>-site-install
```

Once that's wired, the full flow is automatic:

```
secret manager → ESO → install Secret bumps →
  Reloader rolls Deployment → initContainer detects drift →
    wp user update → wp_users caught up to Secret
```

Idempotent and multi-replica safe — only the first Pod to roll runs
`wp user update`; subsequent Pods see the DB in sync and short-circuit.
One DB write, one WP "Password Changed" notification per rotation,
regardless of replica count.

Full setup, verification, failure modes, and the
`syncAdminCredentials: false` opt-out are in
[Operations → Admin credential rotation](/operations/admin-credential-rotation).

The **database password** rotates differently — the Deployment, wpcron
CronJob, and install Job all read `DB_PASSWORD` from `secretKeyRef`, so
when the Secret value changes, restarting pods picks up the new value
automatically. No chart-side reconciliation needed (and Reloader handles
the restart trigger).

### Skipping install

For sites being restored from an existing database dump:

```bash theme={null}
helm install mysite oci://ghcr.io/frankenpress/charts/site \
  --set siteInstall.enabled=false
```

`wp core update-db` is skipped too, so make sure your dump is
schema-compatible with the WP core version baked into the site image.
If it isn't, re-enable the Job after restore — `wp core is-installed`
will short-circuit and only `update-db` will run.

## Production overrides example

```yaml theme={null}
# values-prod.yaml
image:
  repository: ghcr.io/your-org/your-site
  tag: v1.0.0

site:
  url: https://mysite.example.com
  env: production

replicaCount: 3
autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10

ingress:
  enabled: true
  className: nginx
  hostname: mysite.example.com
  tls: true

# Disable in-cluster subcharts.
mariadb:
  enabled: false
redis:
  enabled: false
minio:
  enabled: false

# Point at production services.
externalDatabase:
  host: mysite-mariadb-primary.databases.svc.cluster.local
  database: mysite
  user: mysite
  existingSecret: mysite-db-credentials

externalCache:
  host: mysite-dragonfly.cache.svc.cluster.local
  port: 6379

externalS3:
  bucket: mysite-media-prod
  region: eu-west-1
  bucketUrl: https://cdn.mysite.example.com
  existingSecret: mysite-s3-credentials   # keys: access-key, secret-key

# External Secrets Operator (recommended) for keys+salts.
keysSalts:
  autoGenerate: false
  existingSecret: mysite-wp-keys
```

## CI / publishing

* **`lint.yml`** — `helm lint` + `helm template` + `chart-testing ct lint` on every push and PR.
* **`release.yml`** — on `v*.*.*` tag push, `helm package` + push to `oci://ghcr.io/frankenpress/charts/site:<version>`. Tag-driven, never push-to-main-driven, so `Chart.yaml` `version` and the git tag move together. OCI is the canonical channel — no `gh-pages` fallback.
