> ## 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.

# Troubleshooting

> Common failure modes and how to diagnose them

If you hit something not covered here,
[open an issue](https://github.com/frankenpress/charts/issues) — 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.

```bash theme={null}
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:
   ```bash theme={null}
   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 log** — `error_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 installed** — `roots/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:

```bash theme={null}
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](https://github.com/humanmade/S3-Uploads/issues/587)
and [#158](https://github.com/humanmade/S3-Uploads/issues/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:

```yaml theme={null}
# composer.json
"frankenpress/mu-plugin": "^0.2.0"
```

Confirm the deployed pod has no `FP_S3_OBJECT_ACL` set:

```bash theme={null}
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`:

```php theme={null}
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:

```yaml theme={null}
externalS3:
  objectAcl: public-read   # or `private`, `authenticated-read`
```

For new buckets, see
[Recommended S3 bucket configuration](/operations/production#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](/components/runtime#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 `|`:

```bash theme={null}
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:

```bash theme={null}
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 timing** — `deleted_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:

```bash theme={null}
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:

```text theme={null}
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](/operations/upgrade).

### 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:

```bash theme={null}
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.
