Skip to main content
runtime is the base container image every FrankenPress site extends. It ships: Published images: a multi-arch manifest list (linux/amd64 + linux/arm64) per supported PHP minor — :php8.3, :php8.4, :php8.5. The default channel is ghcr.io/frankenpress/runtime:php8.3. See Runtime matrix for the full tag schema.

Consumer pattern

Downstream sites build on top:
site-template ships this Dockerfile out of the box.

Environment variables

All optional. Defaults produce a working config out of the box.

Logging

The public server emits Caddy access logs as JSON to stdout (one line per request); PHP errors flow to stderr via php.ini. The metrics server is intentionally unlogged. End-to-end shipping setup (Vector → Loki / Datadog) is in Operations → Logging.

Build args

Override at build time with --build-arg:

Cacheability model

Souin caches only anonymous GETs to public paths. Two layers of bypass keep authenticated and admin traffic out of the cache, regardless of whether the upstream Cache-Control header is honoured by Souin itself (cache-handler v0.16.0 is unreliable about no-store / private):

Layer 1 — path bypass

The global cache block uses regex.exclude to skip any path matching:
Covers Bedrock (/wp/...) and classic (/...) layouts. Set FP_CACHE_BYPASS_EXTRA to add custom paths — the value is appended verbatim to the regex, so use an alternation fragment with a leading |:
A server-block matcher routes any request carrying a per-user state cookie around the cache directive entirely:
This is what stops a logged-in admin’s response from being keyed into Redis under the URL anonymous visitors then read — the leak that prompted this design.

What’s still cached

Everything else: anonymous GETs to /, /<slug>/, /category/<x>/, feeds, sitemaps, etc. Per the standard HTTP semantics — upstream Cache-Control is honoured, with FP_CACHE_DEFAULT_CONTROL as the fallback when the response is silent.

Note on cache key scheme

You will see Cache-Status headers like:
Even though the client used https://. This is intentional and benign:
  • Caddy 2.7.x’s trusted_proxies directive controls which upstream hops may set X-Forwarded-For (client IP propagation). It does not propagate X-Forwarded-Proto into the request scheme that Souin reads. So when a TLS-terminating proxy (Envoy Gateway, Cloudflare, an LB) sits in front, Souin sees plain HTTP from inside the cluster and keys cache entries with http regardless of how the client connected.
  • The two failure modes this could matter for are both already closed: authenticated requests are bypassed entirely via the cookie matcher, and invalidation works regardless via mu-plugin v0.2.1+‘s dual-scheme DEL (the SouinInvalidator deletes both GET-http-... and GET-https-... variants on every write).
You can flip the key to https natively with either cache_keys { .+ { disable_scheme } } (drops scheme from the key shape; needs a coordinated invalidator update + cache flush) or an out-of-tree Caddy plugin that rewrites r.URL.Scheme. Neither is worth the churn for what is purely a Cache-Status cosmetic.

Page cache invalidation

Souin caches GET responses in Redis. Invalidation is performed by mu-plugin’s SouinInvalidator, connecting directly to Redis and DELing cache keys. Souin’s documented HTTP invalidation APIs (PURGE, POST-CRUD, /api.souin/* admin) are broken in cache-handler v0.16.0 — the workaround is empirical. Redis key shape (verified):
Bulk tag-based invalidation: read SURROGATE_<tag> (a Redis SET), then pipeline-DEL all members. Sub-millisecond. Full investigation log: runtime/PHASE-0.md.

Local development

CI / publishing

Built and pushed by GitHub Actions on every push to main and on v*.*.* tag pushes. CI exercises the full (php, arch) matrix and emits per-PHP rolling, per-PHP per-sha, per-PHP per-release, and an unprefixed per-release tag (default PHP only). The full schema lives in Runtime matrix.