site-template is
a GitHub template repo
for new WordPress sites running on the FrankenPress stack.
Layout
Bedrock-style:What is Bedrock and why use it?
Bedrock is Rootsβ opinionated shape for WordPress sites: WordPress is treated as a versioned dependency rather than something you commit, the filesystem is laid out for clean separation of code and runtime data, and configuration lives in one place per environment instead of being scattered acrosswp-config.php and the database.
site-template is Bedrock-style β composer-managed, with the
canonical web/wp + web/app + config/ split β because the whole
FrankenPress proposition (immutable image, declarative deploy) breaks
down without it. There needs to be a clear answer to βwhatβs code?β
and βwhatβs data?β before you can confidently bake one into an image
and leave the other in a database / S3 bucket.
What each Roots package does for you
The site composer-requires four small Roots packages. Each one does one thing:| Package | Job |
|---|---|
roots/wordpress | Composer-installable WordPress core β no bundled themes, no sample content. Lands at web/wp/ (gitignored). Lets you treat WP as a versioned dependency. |
roots/wp-config | Thin loader (web/wp-config.php) that defers all config to config/application.php. Single source of truth for site config β plugins canβt mutate it behind your back. |
roots/bedrock-autoloader | Scans web/app/mu-plugins/<dir>/ and loads each oneβs main file. Required because WordPress only auto-loads .php files at the root of mu-plugins/, not in subdirectories β and composer-installed mu-plugins always end up in subdirectories. |
roots/bedrock-disallow-indexing | When DISALLOW_INDEXING is true, forces blog_public = 0 (noindex meta + robots.txt Disallow: /) and adds an admin notice with the current WP_ENV. Fires automatically on staging / development, no-ops on production. |
vlucas/phpdotenv
and oscarotero/env
sit alongside β they read .env into $_ENV for local dev and provide
the env('FOO') helper used throughout application.php. In
production the Helm chart sets env vars directly via the ConfigMap,
so .env isnβt loaded at all.
Bedrock vs the other Roots projects
Roots also publishes Sage (modern build tooling for a custom theme) and Trellis (Ansible-based provisioning for Bedrock sites on VMs). Neither is used by FrankenPress:- Sage is theme-side β orthogonal to the site shape. You can use Sage on top of a FrankenPress site if you like; the runtime doesnβt care which theme you ship.
- Trellis is the operational layer Bedrock sites traditionally paired with β VMs, Ansible, Nginx, PHP-FPM. FrankenPress replaces Trellis: containerised single-process runtime, declarative Helm deploy, immutable image. If youβre coming from a Trellis-shaped Bedrock site, FrankenPress is the migration target β keep Bedrock, drop Trellis.
Forking
Use this template
Click Use this template on the repo. Pick a name (e.g.
mysite).Bootstrap locally
fp init scaffolds .env from .env.example, runs composer install
via docker (no PHP needed on host), brings the stack up
(docker compose up -d --wait), installs WordPress with sensible
local defaults (admin / admin / admin@example.test), and applies
the latest committed snapshot if there is one. Re-run it after
docker compose down -v to recover from a wiped local stack.Visit http://localhost:8080/wp/wp-admin/ and log in (override
credentials via [init] in frankenpress.toml).The
charts Helm chart runs
wp core install automatically as a post-install hook Job on
cluster deploys β thatβs a Helm-only mechanism, so this fp init
flow is only relevant for local Docker Compose dev.Adding plugins and themes
Composer-managed (recommended):composer.json. Plugins land in web/app/plugins/, themes in
web/app/themes/.
Custom code (your own theme or plugin): drop a directory under the
right web/app/* subtree and commit it. The .gitignore ignores
composer-installed content but unhides anything you commit explicitly.
Lockdown
DISALLOW_FILE_EDIT, DISALLOW_FILE_MODS, and DISALLOW_INDIRECT_FILE_MODS
in config/application.php are gated on KUBERNETES_SERVICE_HOST β the
env var the kubelet injects into every Pod:
true β the βUpdate WordPress / plugins / themesβ
buttons are absent and admin-side installs hard-fail. Out-of-cluster
(docker-compose, bare local): all three are false so designers can install
block plugins / evaluation themes locally and promote the result into
the image + DB via wp fp snapshot. Prod canβt accidentally land in the
relaxed mode β the kubelet always sets KUBERNETES_SERVICE_HOST.
DISALLOW_INDIRECT_FILE_MODS (WP 6.4+) is the third flag because the
other two miss indirect write paths β language-pack downloads, font
installs, and Site Health helper writes go through request_filesystem_credentials()
rather than the plugin/theme installer, and would otherwise still land
on ephemeral pod disk.
FP_ALLOW_FILE_MODS=1 is a narrow opt-out used by the chartβs install
Job only (so wp plugin install can run transiently inside the Job for
snapshot apply). Web Pods, wpcron Pods, and init containers never see
it. Setting it flips all three flags back off in lockstep.
Plus in production.php, hard-coded with no gate:
AUTOMATIC_UPDATER_DISABLED covers core / translations / themes / plugins as
a top-level kill switch; the three explicit WP_AUTO_UPDATE_* flags are
defense in depth so a future WP release that loosens the umbrella check
still finds an explicit false for each surface.
The container image is the source of truth in-cluster β admin-side
plugin installs would land on ephemeral disk and disappear on pod
restart, replicating inconsistently across replicas.
Auth keys required at boot
config/application.php lists all eight WordPress auth keys + salts
(AUTH_KEY, SECURE_AUTH_KEY, LOGGED_IN_KEY, NONCE_KEY, plus the
matching _SALT siblings) in Dotenv->required(). A missing value
fails loud at boot β Bedrock raises a RuntimeException before WP is
loaded β instead of silently falling back to 'put your unique phrase here' and degrading session crypto across the cluster.
Local dev keeps the dev-key / dev-salt defaults from
docker-compose.yml. Production must inject real values via your
clusterβs secret manager (the chartβs keysSalts.existingSecret or
External Secrets Operator).
CI / publishing
GitHub Actions:lint.ymlβ PHPCS + composer audit on every push and PRbuild.ymlβ on push to main and onv*.*.*tag, builds the site image and pushes toghcr.io/<your-org>/<your-site>
ghcr.io/<your-org>/<your-site>:v1.0.0.
Deploy with: