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:
| Component | Version | Notes |
|---|
| FrankenPHP | 1.12.2 | Caddy + PHP runtime, single statically-linked binary. Built per supported PHP minor — see Runtime matrix |
| caddyserver/cache-handler | v0.16.0 | Souin HTTP cache, compiled into the FrankenPHP binary via xcaddy |
| darkweak/storages/go-redis/caddy | v0.0.19 | Redis storage backend for Souin |
| dunglas/caddy-cbrotli | v1.0.1 | Brotli encoding |
| WP-CLI | 2.12.0 | Cron + admin tasks |
| PHP extensions | gd, intl, exif, zip, opcache, mysqli, pdo_mysql, memcached, redis | WP-friendly set |
| mu-plugin | configurable | Baked 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.
| Var | Default | Purpose |
|---|
REDIS_URL | redis:6379 | Address of the Redis used by Souin’s HTTP cache |
FP_CACHE_TTL | 5m | Default cache entry TTL |
FP_CACHE_STALE | 1h | Stale-while-revalidate window |
FP_CACHE_DEFAULT_CONTROL | public, s-maxage=300 | Cache-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/web | WordPress webroot inside the container |
FP_PORT | 8080 | Public HTTP listen port |
FP_METRICS_PORT | 9145 | Prometheus 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:
| Arg | Default |
|---|
PHP_VERSION | 8.3 |
FRANKENPHP_VERSION | 1.12.2 |
CACHE_HANDLER_VERSION | v0.16.0 |
STORAGES_GO_REDIS_VERSION | v0.0.19 |
CADDY_CBROTLI_VERSION | v1.0.1 |
WP_CLI_VERSION | 2.12.0 |
FP_MU_PLUGIN_VERSION | v0.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/"
Layer 2 — cookie bypass
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.