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.

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: edit 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.
The commands below use 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.
brew install composer
Verify:
composer --version
# Composer version 2.x.x ...
Most distros package an outdated Composer; the official installer is safer:
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php
composer --version
See the official install guide for a checksum-verified flow.
Use the Composer-Setup.exe installer from getcomposer.org. Run the rest of the commands on this page in Git Bash / WSL / PowerShell — adjust quoting as needed.
If you can’t or don’t want to install Composer, the official composer:2 Docker image is a drop-in. Replace composer <args> with:
docker run --rm -v "$PWD:/app" -w /app composer:2 <args>
The rest of this guide assumes you have 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:
TypeComposer nameInstalls to
Pluginwpackagist-plugin/<slug>web/app/plugins/<slug>/
Must-use pluginwpackagist-plugin/<slug> (with installer override)web/app/mu-plugins/<slug>/
Themewpackagist-theme/<slug>web/app/themes/<slug>/
The <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.
1

Branch your site repo

git -C mysite checkout main
git -C mysite pull --ff-only
git -C mysite checkout -b feat/add-classic-editor
2

Add the plugin via Composer

Run composer require with the wpackagist slug:
cd mysite
composer require wpackagist-plugin/classic-editor:^1.6
This updates 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.
3

Commit and open a PR

Only composer.json should have changed. Commit, push, open a PR:
git -C mysite add composer.json
git -C mysite commit -m "feat: add Classic Editor plugin"
git -C mysite push -u origin feat/add-classic-editor
gh pr create
4

Wait for CI, merge, tag a release

Once CI is green and the PR is merged, tag a new release. The tag is what triggers the image build:
# Pull the squash-merged commit into local main
git -C mysite checkout main
git -C mysite pull --ff-only

# Tag and push (CI builds + pushes the image to GHCR)
git -C mysite tag -a v0.1.6 -m "feat: add Classic Editor"
git -C mysite push origin v0.1.6
Watch the build:
gh run watch --repo <owner>/mysite
5

Update your GitOps values

Bump image.tag in the values file your cluster pulls from. With ArgoCD watching a Git repo:
# gitops/.../values.yaml
fp-site:
  image:
    tag: 0.1.6   # was 0.1.5
Commit + push that. ArgoCD will sync on its next reconcile (a few minutes by default) and roll the site Deployment.If you’re using helm upgrade directly instead of GitOps:
helm upgrade <release> oci://ghcr.io/eightoeight/charts/fp-site \
  --reuse-values --set image.tag=0.1.6
6

Activate the plugin

The plugin’s files are now in the running pod, but WordPress doesn’t auto-activate plugins. Activation is stored in the wp_options.active_plugins database row — DB state, not image state. Activate it once with wp-cli:
kubectl -n <ns> exec deploy/<release>-fp-site -- \
  wp plugin activate classic-editor --allow-root --path=/app/web/wp
Activation persists across redeploys and replica scaling because the database persists. You only run this command once per plugin, not per release.
7

Verify

# Plugin is installed AND active
kubectl -n <ns> exec deploy/<release>-fp-site -- \
  wp plugin list --allow-root --path=/app/web/wp \
  | grep classic-editor
# expect: classic-editor    active   none    1.6.7
Then visit the WordPress admin: Posts → Add New should now open the classic editor instead of Gutenberg.

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_options rows / custom tables on deactivation; only on uninstall (which the lockdown also blocks from the admin). Use wp plugin uninstall if you want a thorough scrub.
1

Deactivate via wp-cli (against the live cluster)

kubectl -n <ns> exec deploy/<release>-fp-site -- \
  wp plugin deactivate classic-editor --allow-root --path=/app/web/wp
Optional: also run 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.)
2

Remove from composer.json on a branch

git -C mysite checkout main && git -C mysite pull --ff-only
git -C mysite checkout -b chore/remove-classic-editor

cd mysite
composer remove wpackagist-plugin/classic-editor
3

PR, merge, tag, GitOps bump

Same shape as the add flow:
git -C mysite add composer.json
git -C mysite commit -m "chore: remove Classic Editor plugin"
git -C mysite push -u origin chore/remove-classic-editor
gh pr create

# After merge:
git -C mysite checkout main && git -C mysite pull --ff-only
git -C mysite tag -a v0.1.7 -m "chore: remove Classic Editor"
git -C mysite push origin v0.1.7
Then bump image.tag to 0.1.7 in your GitOps values.yaml.
4

Verify

Once ArgoCD has synced and the new pods are up:
kubectl -n <ns> exec deploy/<release>-fp-site -- \
  wp plugin list --allow-root --path=/app/web/wp \
  | grep classic-editor || echo "classic-editor removed"

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.
1

Branch and add via Composer

git -C mysite checkout main && git -C mysite pull --ff-only
git -C mysite checkout -b feat/add-twentytwentythree

cd mysite
composer require wpackagist-theme/twentytwentythree:^1.5
The theme files land in web/app/themes/twentytwentythree/ at image build time.
2

Commit, PR, merge, tag, GitOps bump

Same as the plugin flow — git add composer.json, PR, merge, tag, bump image.tag in GitOps values.yaml.
3

Activate the theme

A site can have many installed themes but only one active at a time. Activating swaps to the new theme:
kubectl -n <ns> exec deploy/<release>-fp-site -- \
  wp theme activate twentytwentythree --allow-root --path=/app/web/wp
The active theme is stored in 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.
4

Verify

kubectl -n <ns> exec deploy/<release>-fp-site -- \
  wp theme list --status=active --allow-root --path=/app/web/wp
# expect: twentytwentythree   active   ...
Visit the public site — the look-and-feel reflects the new theme.

Remove a theme

The big rule: you can’t remove the currently-active theme. WordPress requires at least one valid theme in wp-content/themes/ that matches wp_options.template, or it errors out. Switch to a different theme first.
1

Switch to a different active theme first

kubectl -n <ns> exec deploy/<release>-fp-site -- \
  wp theme activate <other-theme> --allow-root --path=/app/web/wp
Confirm the public site renders correctly under the new theme before continuing — this is the most-likely point of failure.
2

Remove from composer.json on a branch

git -C mysite checkout main && git -C mysite pull --ff-only
git -C mysite checkout -b chore/remove-twentytwentythree

cd mysite
composer remove wpackagist-theme/twentytwentythree
3

PR, merge, tag, GitOps bump

Same shape. Tag the next patch version, bump image.tag in GitOps.
4

Verify

After the new pods are up:
kubectl -n <ns> exec deploy/<release>-fp-site -- \
  wp theme list --allow-root --path=/app/web/wp \
  | grep twentytwentythree || echo "twentytwentythree removed"

Common gotchas

Pods can run a cached image if they happened to schedule on a node that already had it. Force a rollout to be sure:
kubectl -n <ns> rollout restart deploy/<release>-fp-site
Confirm with:
kubectl -n <ns> get pods -l app.kubernetes.io/name=fp-site \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}'
Always pass --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.
The image build didn’t include the plugin — usually because the image rolled out is older than the one with your composer.json change. Double-check the running pods’ image matches the tag you bumped to in GitOps.
Sites with paid or self-hosted plugins use a custom Composer repository entry in composer.json — typically a private VCS repo or a Satis / Composer Asset Server instance. Out of scope here; see the Roots Bedrock Composer guide.

Companion docs