Skip to main content
If you hit something not covered here, open an issue — we treat docs gaps as real bugs.

Pod symptoms

ErrImageNeverPull for the site pod

You set image.pullPolicy=Never (typical for kind dev) but the image isn’t loaded into the cluster.
kind load docker-image <your-image>:<tag> --name <cluster-name>
kubectl --namespace <ns> rollout restart deploy/<release>-site

ImagePullBackOff on bitnami/* images

Bitnami withdrew their public Docker Hub images in 2025. The chart’s default values point at bitnamilegacy/* — verify yours match. If you override mariadb.image.repository etc., make sure the override still exists.

CreateContainerConfigError: secret not found

The site pod references a Secret that doesn’t exist yet. Common causes:
  1. You set keysSalts.existingSecret but the named Secret hasn’t been created. Either create it (External Secrets Operator, kubectl, etc.) or set keysSalts.autoGenerate: true.
  2. You disabled a subchart (mariadb.enabled: false) but didn’t supply externalDatabase.existingSecret for the password.
  3. Subchart resource naming mismatch — bitnami subcharts produce <release>-<subchart> for their resources, not <release>-<our-chart>-<subchart>. Our _helpers.tpl accounts for this; if you’ve overridden nameOverride or fullnameOverride it might drift.

exec: frankenphp: Operation not permitted

You’re running with containerSecurityContext.allowPrivilegeEscalation: false against an runtime image older than the setcap -r patch. Older images carry cap_net_bind_service=ep on the binary; the kernel refuses to exec under no_new_privs. Fix: upgrade to a newer runtime (the cap is stripped at build time on current images), or temporarily set allowPrivilegeEscalation: true in your values.

Cache symptoms

Souin returns stale content after save_post

The SouinInvalidator is supposed to DEL the cache key on save. If content is stale beyond the TTL window:
  1. Verify Redis connectivity — exec into the site pod and try to ping Redis manually:
    kubectl exec deploy/<release>-site -- php -r 'echo (new Redis())->connect(getenv("FP_SOUIN_REDIS_HOST"), (int)getenv("FP_SOUIN_REDIS_PORT")) ? "ok" : "fail";'
    
  2. Check the WP error logerror_log lines from [FrankenPress\\SouinInvalidator] indicate connection or DEL failures.
  3. Verify the cache key shape — Souin uses GET-<scheme>-<host>-<path>. If the WP scheme/host returned by home_url() differs from what Souin sees on the request, the keys diverge. Set WP_HOME and WP_SITEURL to exactly the externally-visible URL (including scheme), and ensure the Ingress / HTTPRoute passes X-Forwarded-Proto correctly.

Cache-Status: Souin; fwd=bypass; detail=UPSTREAM-ERROR-OR-EMPTY-RESPONSE

WordPress returned an empty response. Common causes:
  • No theme installedroots/wordpress is no-content (no bundled themes). Make sure the site composer-installs at least one theme (site-template ships wpackagist-theme/twentytwentyfive by default).
  • PHP fatal during request — check the site pod logs for stack traces.
  • WP not yet installed — the install Job hasn’t completed yet (or is disabled). Check kubectl --namespace <ns> get jobs for the <release>-site-install Job; tail the most recent pod’s logs for the failure. If siteInstall.enabled=false, run wp core install manually via kubectl exec.

S3 symptoms

”FrankenPress: media uploads disabled” error in WP admin

S3UploadsBootstrap registered wp_handle_upload_prefilter to refuse uploads. The chart’s ConfigMap is missing one of the required env vars (FP_S3_BUCKET, FP_S3_KEY, or FP_S3_SECRET). Check:
kubectl --namespace <ns> get configmap <release>-site-env -o yaml | grep FP_S3
For local kind dev with the bundled MinIO subchart, these are auto-set from the MinIO Secret. If they’re empty, the helm install probably had a values override that disabled MinIO without supplying externalS3.*.

InvalidAccessKeyId from S3 SDK

The AWS SDK is hitting s3.amazonaws.com with credentials that don’t exist there. Two common causes:
  1. FP_S3_ENDPOINT not honoured — mu-plugin v0.1.0 had a bug where humanmade/s3-uploads’ S3_UPLOADS_ENDPOINT constant was defined but not applied to the SDK. Upgrade to v0.1.1 or later (the bake default in runtime).
  2. Real AWS credentials misconfigured — verify FP_S3_KEY and FP_S3_SECRET are correct, and the IAM principal has s3:GetObject / s3:PutObject / s3:DeleteObject / s3:ListBucket on the bucket.

0-byte uploads to S3

Media uploads land in S3 with the right key + path, but Content-Length: 0 and ETag d41d8cd98f00b204e9800998ecf8427e (the MD5 of an empty file). WordPress shows the attachment as broken; thumbnails are also empty. Cause: humanmade/s3-uploads sends x-amz-acl: public-read on every PUT by default. Buckets with Object Ownership = “Bucket owner enforced” (the AWS new-bucket default since April 2023, ACLs disabled) reject the ACL header and abort the PUT mid-stream, leaving the 0-byte object behind. Same root cause as humanmade/S3-Uploads#587 and #158. Fix: on mu-plugin v0.2.0+ this is the default — FP_S3_OBJECT_ACL is unset, the bootstrap defines S3_UPLOADS_OBJECT_ACL as null, and no ACL header is sent. Bump:
# composer.json
"frankenpress/mu-plugin": "^0.2.0"
Confirm the deployed pod has no FP_S3_OBJECT_ACL set:
kubectl --namespace <ns> get configmap <release>-site-env -o yaml | grep FP_S3_OBJECT_ACL
# Expected: no output
If you can’t bump the dep right away, drop a one-line override into the site’s config/application.php:
Config::define( 'S3_UPLOADS_OBJECT_ACL', null );
If the bucket genuinely needs ACLs (older bucket with Object Ownership = “Object writer” or “Bucket owner preferred”), opt back in via the chart:
externalS3:
  objectAcl: public-read   # or `private`, `authenticated-read`
For new buckets, see Recommended S3 bucket configuration.

Logged-out browser sees logged-in admin content

Admin pages were being served from cache. WP signals Cache-Control: no-store, private on /wp-admin/ and the REST API, but cache-handler v0.16.0 doesn’t reliably honour those directives. Fixed in runtime v0.1.4+: the global cache block uses regex.exclude to skip every auth-aware path (/wp-admin/, /wp-login.php, /wp-cron.php, /xmlrpc.php, /wp-json/, including Bedrock /wp/... variants), and a server-block matcher routes any request carrying a wordpress_logged_in_*, wp-postpass_*, or comment_author_* cookie around the cache directive entirely. See Cacheability model. If you have custom auth-aware paths (membership area, signed-media endpoints), set FP_CACHE_BYPASS_EXTRA — appended verbatim to the bypass regex, so use an alternation fragment with a leading |:
FP_CACHE_BYPASS_EXTRA="|^/api/private/|^/members/"
If you’re still on an older runtime and need an immediate flush, drop the cached admin keys directly:
kubectl -n <ns> exec <redis-pod> -- redis-cli --scan --pattern 'GET-*' \
  | xargs -r kubectl -n <ns> exec <redis-pod> -- redis-cli DEL

Edits saved but not reflected on view

Post edit / trash / permanent-delete in wp-admin appears to succeed (“Saved” / “Moved to trash”) but the public URL keeps serving the old content until the cache TTL expires. Two distinct bugs converged in older releases:
  1. Scheme mismatch — Souin keyed cache entries with http while WordPress emitted https://... URLs for invalidation. Fixed in runtime v0.1.3+ (Caddy now trusts the upstream proxy’s X-Forwarded-Proto) and mu-plugin v0.2.1+ (DELs both schemes defensively).
  2. Hook timingdeleted_post fires AFTER the row is gone, so get_permalink( $post_id ) returns false and the invalidator no-ops. Fixed in mu-plugin v0.2.2+ (also hooks wp_trash_post and before_delete_post, which fire BEFORE the state change with the permalink still resolvable).
Bump both — the changes are co-required for full coverage.

Helm symptoms

INSTALLATION FAILED: ... namespace ... is being terminated

You uninstalled but the namespace is still deleting. Wait, then retry:
until ! kubectl get namespace <ns> >/dev/null 2>&1; do sleep 2; done
helm install ...

Error: looks like "..." is not a valid chart repository

You’re using OCI registry syntax (oci://...) on a Helm version older than 3.8. Upgrade Helm: brew upgrade helm (or your platform equivalent).

helm dependency update: image pull failure

Bitnami’s OCI registry occasionally rate-limits anonymous pulls. Run helm dependency update again, or authenticate to the registry first with helm registry login registry-1.docker.io.

WordPress symptoms

Bedrock-autoloader DB errors during wp core install

You’ll see error lines like:
WordPress database error Table 'wordpress.wp_options' doesn't exist
  for query INSERT INTO `wp_options` ... bedrock_autoloader ...
These appear during the first wp core install because roots/bedrock-autoloader tries to write its discovery cache to wp_options before WP creates the table. The install completes successfully despite the noise; subsequent boots are clean. Not blocking; ignore.

”Update WordPress” button is grayed-out / missing

This is by design. The lockdown constants (DISALLOW_FILE_MODS, AUTOMATIC_UPDATER_DISABLED) are hard-coded in config/application.php and config/environments/production.php. Updates go through image rebuilds — see Upgrading.

Browser shows “unknown app” / 301 redirect loop on localhost:8080

You’re port-forwarding to the site Service but the browser ends up at http://site.localhost/ (or http://localhost/) and OrbStack / your DNS resolver shows “unknown app” or a connection-refused page. The chart defaults WP_HOME to http://site.localhost, and WordPress 301-redirects any request whose Host header doesn’t match. When you kubectl port-forward to localhost:8080, the browser sends Host: localhost:8080, which WP rejects. Fix it by setting site.url to the URL you’ll actually browse to:
helm upgrade <release> oci://ghcr.io/frankenpress/charts/site \
  --namespace <ns> --reuse-values \
  --set site.url=http://localhost:8080
Wait for the rollout, then port-forward svc/<release>-site 8080:80 and browse to http://localhost:8080/. For a real deploy, set site.url to your public URL and enable ingress.* or httpRoute.* instead of port-forwarding.