Skip to main content

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.

runtime is the base container image every FrankenPress site extends. It ships:
ComponentVersionNotes
FrankenPHP1.12.2Caddy + PHP runtime, single statically-linked binary. Built per supported PHP minor — see Runtime matrix
caddyserver/cache-handlerv0.16.0Souin HTTP cache, compiled into the FrankenPHP binary via xcaddy
darkweak/storages/go-redis/caddyv0.0.19Redis storage backend for Souin
dunglas/caddy-cbrotliv1.0.1Brotli encoding
WP-CLI2.12.0Cron + admin tasks
PHP extensionsgd, intl, exif, zip, opcache, mysqli, pdo_mysql, memcached, redisWP-friendly set
mu-pluginconfigurableBaked at build time via FP_MU_PLUGIN_VERSION (default: v0.1.1)
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:
# In your-site/Dockerfile
FROM composer:2 AS deps
COPY composer.* /app/
WORKDIR /app
RUN composer install --no-dev --no-scripts --optimize-autoloader

FROM ghcr.io/frankenpress/runtime:php8.3
COPY --from=deps /app/vendor /app/vendor
COPY web /app/web
COPY config /app/config
site-template ships this Dockerfile out of the box.

Environment variables

All optional. Defaults produce a working config out of the box.
VarDefaultPurpose
REDIS_URLredis:6379Address of the Redis used by Souin’s HTTP cache
FP_CACHE_TTL5mDefault cache entry TTL
FP_CACHE_STALE1hStale-while-revalidate window
FP_CACHE_DEFAULT_CONTROLpublic, s-maxage=300Cache-Control fallback when upstream doesn’t set one
FP_CACHE_BYPASS_EXTRA(empty)Extra alternation fragment appended to the path-bypass regex. See Cacheability model.
FP_DOCROOT/app/webWordPress webroot inside the container
FP_PORT8080Public HTTP listen port
FP_METRICS_PORT9145Prometheus metrics listen port (separate from public traffic)

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:
ArgDefault
PHP_VERSION8.3
FRANKENPHP_VERSION1.12.2
CACHE_HANDLER_VERSIONv0.16.0
STORAGES_GO_REDIS_VERSIONv0.0.19
CADDY_CBROTLI_VERSIONv1.0.1
WP_CLI_VERSION2.12.0
FP_MU_PLUGIN_VERSIONv0.1.1 (set to "" to skip baking)

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:
^(/wp)?/(wp-admin/|wp-login\.php|wp-cron\.php|xmlrpc\.php|wp-json/)
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 |:
FP_CACHE_BYPASS_EXTRA="|^/api/private/|^/members/"
A server-block matcher routes any request carrying a per-user state cookie around the cache directive entirely:
@auth_or_state_cookie header_regexp Cookie (wordpress_logged_in_|wp-postpass_|comment_author_)
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:
cache-status: Souin; hit; key=GET-http-frankenpress.com-/
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):
GET-<scheme>-<host>-<path>     cached response body
IDX_GET-<scheme>-<host>-<path> index entry pointing at the body
SURROGATE_<tag>                set of cache keys for a given Surrogate-Key tag
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

git clone https://github.com/frankenpress/runtime
cd runtime
make build         # build runtime:dev
make up            # docker compose up -d (runtime + redis)
make test          # run tests/cache-spike.sh
make down          # tear down
make ci            # all of the above in one shot

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.