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) |
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.| 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 viaphp.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 upstreamCache-Control header is honoured by Souin
itself (cache-handler v0.16.0 is unreliable about no-store /
private):
Layer 1 — path bypass
The globalcache block uses regex.exclude to skip any path matching:
/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
|:
Layer 2 — cookie bypass
A server-block matcher routes any request carrying a per-user state cookie around thecache directive entirely:
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 seeCache-Status headers like:
https://. This is intentional and
benign:
- Caddy 2.7.x’s
trusted_proxiesdirective controls which upstream hops may setX-Forwarded-For(client IP propagation). It does not propagateX-Forwarded-Protointo 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 withhttpregardless 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
SouinInvalidatordeletes bothGET-http-...andGET-https-...variants on every write).
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 bymu-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):
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 tomain 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.