site Helm install bundles in-cluster MariaDB + Redis +
MinIO subcharts so a helm install works on kind with zero
prerequisites. These defaults are not for production. This page
documents the recommended swaps.
Production swap matrix
| Component | Default (dev / kind) | Recommended for production |
|---|---|---|
| Database | bitnami/mariadb subchart | MariaDB Operator |
| HTTP cache | bitnami/redis subchart | DragonflyDB Operator |
| Object storage | bitnami/minio subchart | AWS S3 / Cloudflare R2 / GCS XML |
| WP keys+salts | auto-generated Job | External Secrets Operator |
Why swap?
MariaDB Operator vs the bundled subchart
MariaDB Operator vs the bundled subchart
The bundled
bitnami/mariadb is a single-replica StatefulSet
with no automatic backups, no failover, no point-in-time recovery,
and no schema migration tooling. Fine for kind, dangerous for
production.MariaDB Operator
provides a declarative MariaDB custom resource with replication,
backups (Galera, mariabackup, mysqldump), User / Database /
Grant CRDs, and Galera cluster mode for HA.DragonflyDB vs Redis
DragonflyDB vs Redis
Souin uses the RESP protocol to talk to its cache backend.
DragonflyDB is a drop-in Redis-protocol-compatible
in-memory store with dramatically better single-node throughput
(Dragonfly’s docs cite ~25× over Redis on multi-core hardware
because of its lock-free shared-nothing architecture).The Dragonfly Operator
deploys it declaratively. From FrankenPress’s perspective it’s just
a Redis-protocol endpoint — set
externalCache.host and we don’t
care what’s actually behind the address.AWS S3 (or R2 / GCS) vs in-cluster MinIO
AWS S3 (or R2 / GCS) vs in-cluster MinIO
Self-hosted object storage in your cluster is operationally costly
(replication, backups, the data plane is on the same critical path
as everything else). A managed S3-compatible service (AWS S3,
Cloudflare R2, Google Cloud Storage XML, Backblaze B2) takes that
operational burden off you and is usually cheaper at WordPress-site
scale.
humanmade/s3-uploads (which mu-plugin configures) talks the
S3 API; it doesn’t care which provider answers.External Secrets Operator vs the auto-generated Job
External Secrets Operator vs the auto-generated Job
The chart’s default
keysSalts.autoGenerate: true runs a one-shot
Job that creates a Secret containing the eight WP auth keys +
salts on first install. Fine for instant deploy, but for production
you want secrets to live in your cloud secret manager (AWS Secrets
Manager, GCP Secret Manager, HashiCorp Vault, 1Password Connect,
etc.).Set keysSalts.autoGenerate: false and keysSalts.existingSecret: <name>, then have ESO sync the values into that Secret.Recommended S3 bucket configuration
When you swap the bundled MinIO subchart for a real S3 (or R2 / GCS XML) bucket, configure it as below. These defaults match whatmu-plugin v0.2.0+ expects.
Bucket settings
| Setting | Value | Why |
|---|---|---|
| Object Ownership | Bucket owner enforced (ACLs disabled) | The AWS new-bucket default since April 2023. mu-plugin v0.2.0+ omits the x-amz-acl header by default, which is exactly what this mode requires. Mismatched ACL settings on a bucket-owner-enforced bucket cause every PUT to silently abort with a 0-byte object. |
| Block Public Access | Block all public access — on if media is served via CDN; off only if the bucket itself is the public origin | Modern WP sites front media with CloudFront / Cloudflare and grant the CDN access via OAC / a bucket policy, not via per-object public ACLs. |
| Versioning | Enabled | Cheap insurance against accidental overwrites or deletes. WP attachment edits (rotation, scaling) overwrite the original key on the s3-uploads stream wrapper. |
| Default encryption | SSE-S3 (AES256) | The AWS default. SSE-KMS is fine too; KMS adds per-request cost and IAM friction for marginal threat-model benefit on public-by-design media. |
| Region | Co-located with the cluster | Avoids cross-region GET latency on every cache miss + every WP-admin media library load. |
Block Public Access — pick one
Block-public-access has four sub-toggles. Two scenarios:- CDN-fronted (recommended): all four toggles on. CloudFront / Cloudflare reaches the bucket via an Origin Access Control (CloudFront) or a bucket policy that whitelists the CDN’s IP ranges / service-account.
- Bucket as public origin: “Block public ACLs” can stay on (we
don’t set ACLs anyway in v0.2.0+); “Block public bucket policies”
must be off, and you attach a
s3:GetObjectpolicy for*/Principal: "*".
IAM policy (minimum required)
The IAM principal humanmade/s3-uploads authenticates as needs:s3:PutObjectAclis not needed — the v0.2.0+ default sends no ACL header. Granting it doesn’t hurt but it’s pointless on a bucket-owner-enforced bucket (the API call would fail withAccessControlListNotSupported).s3:GetBucketLocationis not needed — humanmade/s3-uploads doesn’t issuegetBucketLocation; we set the region via theFP_S3_REGIONenv var.
CORS (only if you serve media on a different origin)
IfbucketUrl differs from WP_HOME (CDN domain, separate static
host) and the WP admin’s media library uses a XHR.responseType = "blob" flow against the bucket URL, browsers enforce CORS on the
fetch:
<img src> (no CORS preflight) and the admin renders thumbnails
server-side. Add the policy only when a console error surfaces.
Quick verification after creating the bucket
Migrating auto-generated Secrets to a secret manager
If you ranhelm install with the chart’s defaults you already have a
working site backed by auto-generated WP keys+salts and admin
credentials, stored in two Secrets:
<release>-site-keys— the eight WP auth keys and salts<release>-site-install— the wp-admin credentials (whensiteInstall.autoGenerate: true)
Extract WP keys+salts
JSON object ready to paste into a single AWS Secrets Manager / GCP Secret Manager / Vault secret:AUTH_KEY,
SECURE_AUTH_KEY, …, NONCE_SALT):
KEY=value-per-line if you’d rather pipe into something else:
Extract admin install credentials
Same shape:admin_user / admin_email / admin_password. Push those into
your secret manager, then:
Other Secrets
Samekubectl get secret -o json | jq pattern works for every Secret
the chart consumes:
| Auto-generated Secret | Used by | Migrate to |
|---|---|---|
<release>-site-keys | All pods (env: 8 keys+salts) | keysSalts.existingSecret |
<release>-site-install | Install Job (admin creds) | siteInstall.existingSecret |
<release>-mariadb | All pods (DB_PASSWORD) | externalDatabase.existingSecret (after disabling the subchart) |
<release>-minio | All pods (FP_S3_KEY/FP_S3_SECRET) | externalS3.existingSecret (keys: access-key, secret-key) |
enabled: true — for production you’ll provision
the DB / object store separately and supply credentials via your secret
manager from day one.
End-to-end production values
restricted:latest —
runAsNonRoot: true, runAsUser: 33, fsGroup: 33, seccompProfile.type: RuntimeDefault at the pod level; allowPrivilegeEscalation: false,
readOnlyRootFilesystem: true, capabilities.drop: [ALL] per container — so
production overrides above don’t need to repeat them. If you build a custom
runtime that re-adds the cap_net_bind_service file capability, override
containerSecurityContext.allowPrivilegeEscalation: true and
containerSecurityContext.capabilities.add: [NET_BIND_SERVICE] (the kernel’s
no_new_privs flag would otherwise refuse to exec the binary). The published
runtime strips the cap, so this only matters for forked images.
Install:
Image promotion across environments
Multi-site
For multiple sites in one cluster, each gets its own:- Namespace
- Helm release (
helm install <release-name> ...) - DB / cache / S3 endpoints (or shared infrastructure with per-site DBs / buckets)
- Secret with WP keys+salts