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:
helm install mysite oci://ghcr.io/frankenpress/charts/site \
--version 0.13.2 \
--namespace mysite --create-namespace
For the kind-cluster quickstart, see Quickstart.
For the production topology (DragonflyDB Operator, MariaDB Operator,
AWS S3, External Secrets), see Operations → Production
topology.
What gets deployed
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 for credentials and
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. |
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.
Values reference
The full annotated values reference is in
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 for self-driving rotation. Set false to skip — see 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. Transport-agnostic — Postmark, SendGrid, Mailgun, AWS SES, in-cluster relay. See 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 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. 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. 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.
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.
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 upgrades.
Default behavior
By default, the chart creates a <release>-site-install Secret with
a random 32-char admin password. Retrieve it after install:
kubectl --namespace mysite get secret mysite-install \
-o jsonpath='{.data.admin_password}' | base64 -d
Override the username, email, or password at install:
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:
siteInstall:
existingSecret: mysite-admin
existingSecretAdminUserKey: admin_user # default
existingSecretAdminEmailKey: admin_email # default
existingSecretAdminPasswordKey: admin_password # default
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.
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
so the Pod actually rolls when the Secret changes:
# 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.
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:
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
# 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.