WordPress on FrankenPress is immutable at runtime. Code (WP core, plugins, themes, custom files) is baked into the site image at build time; the running container’s filesystem is read-only. The wp-admin “Add new” / “Upload” / “Activate” buttons that install plugins and themes from the dashboard are absent on purpose — anything they wrote would land on ephemeral container disk and vanish on the next pod restart, or replicate inconsistently across replicas. That trades convenience for two big wins: every site is reproducible from a tagged commit, and the same image can be promoted from staging to production with confidence. This page walks through the four common changes: All four follow the same shape: editDocumentation Index
Fetch the complete documentation index at: https://docs.frankenpress.com/llms.txt
Use this file to discover all available pages before exploring further.
composer.json on a branch, ship
a new image tag, point the cluster at it, and (for plugins/themes)
run a one-time wp command to flip the wp_options state in the
database. The flow assumes you’re using the standard FrankenPress
release flow — branches, tag-driven CI, GitOps for the cluster
config. Adapt the cluster step to your own deploy mechanism if you’re
not.
mysite as the example name for your
site repo’s local directory — substitute whatever your fork is
called (e.g. my-blog, acme-marketing, etc.). Same for <ns> /
<release> in the cluster commands — your Kubernetes namespace
and Helm release name.Prerequisites
You’ll need Composer installed locally to add or remove packages. If you already have it, skip ahead.macOS
macOS
Linux
Linux
Windows
Windows
No Composer? Use the Docker image
No Composer? Use the Docker image
composer:2 Docker image
is a drop-in. Replace composer <args> with:composer on $PATH.Where plugins and themes come from
FrankenPress sites use Composer with the wpackagist repository, so every plugin and theme on WordPress.org is composer-installable by slug:| Type | Composer name | Installs to |
|---|---|---|
| Plugin | wpackagist-plugin/<slug> | web/app/plugins/<slug>/ |
| Must-use plugin | wpackagist-plugin/<slug> (with installer override) | web/app/mu-plugins/<slug>/ |
| Theme | wpackagist-theme/<slug> | web/app/themes/<slug>/ |
<slug> is the URL-slug from wordpress.org/plugins/<slug>/ or
wordpress.org/themes/<slug>/. Premium plugins / themes from
elsewhere need a private repository entry in composer.json —
out of scope for this page; see the
Composer + WordPress guide
from the Roots project.
Add a plugin
We’ll add the Classic Editor plugin (maintained by the WP core team — restores the pre-Gutenberg editor for posts and pages). The flow is identical for any other plugin from WordPress.org.Add the plugin via Composer
composer require with the wpackagist slug:composer.json. The plugin lands in
web/app/plugins/classic-editor/ once Composer installs, but
you don’t need that locally — the next image build will install it.composer.lock is in .gitignore by default in
fp-site-template — every build resolves to the latest version
matching your constraint. Pin tightly (use ^1.6 not *) to
avoid surprise upgrades. Sites that want full reproducibility
can remove composer.lock from .gitignore and commit it.Wait for CI, merge, tag a release
Update your GitOps values
image.tag in the values file your cluster pulls from. With
ArgoCD watching a Git repo:helm upgrade directly instead of GitOps:Activate the plugin
wp_options.active_plugins database row — DB state, not image
state. Activate it once with wp-cli:Remove a plugin
The reverse of the add flow. Two extra rules:- Deactivate before removing the files. WordPress tolerates a
plugin disappearing from disk while still listed in
active_plugins, but it produces an admin notice on every load. Cleaner to deactivate first. - The DB still remembers the plugin’s settings. Most plugins
don’t clean up their
wp_optionsrows / custom tables on deactivation; only on uninstall (which the lockdown also blocks from the admin). Usewp plugin uninstallif you want a thorough scrub.
Deactivate via wp-cli (against the live cluster)
wp plugin uninstall classic-editor if you
want the plugin’s DB state cleaned up. (uninstall is safe to
run before file removal — it just runs the plugin’s
register_uninstall_hook callback if defined.)PR, merge, tag, GitOps bump
image.tag to 0.1.7 in your GitOps values.yaml.Add a theme
Themes follow the same shape as plugins, but live in a different composer/disk path. We’ll use the Twenty Twenty-Three theme as the example.Branch and add via Composer
web/app/themes/twentytwentythree/ at
image build time.Commit, PR, merge, tag, GitOps bump
git add composer.json, PR, merge, tag,
bump image.tag in GitOps values.yaml.Activate the theme
wp_options.template /
wp_options.stylesheet — DB state. You can switch back at any
time without rebuilding the image, as long as the target theme is
in the image.Remove a theme
The big rule: you can’t remove the currently-active theme. WordPress requires at least one valid theme inwp-content/themes/
that matches wp_options.template, or it errors out. Switch to a
different theme first.
Switch to a different active theme first
Common gotchas
The site image is stale after deploy
The site image is stale after deploy
`wp` returns 'This does not seem to be a WordPress installation'
`wp` returns 'This does not seem to be a WordPress installation'
--path=/app/web/wp and --allow-root to wp inside
the container. The default working directory is /app, but
WordPress core lives at /app/web/wp/ per the Bedrock layout.Plugin activation fails with 'plugin file does not exist'
Plugin activation fails with 'plugin file does not exist'
composer.json
change. Double-check the running pods’
image matches the tag you bumped to in GitOps.Premium / non-WordPress.org plugins
Premium / non-WordPress.org plugins
Companion docs
- Architecture — why the image is immutable
- Components → fp-site-template — the Bedrock-shaped repo this all targets
- Operations → upgrade — bumping the platform itself (runtime, mu-plugin, chart) follows the same release flow