Plugin & theme provisioning
Because the image is immutable (DISALLOW_FILE_MODS), nothing is installed or updated from
the WordPress admin. Every plugin and theme is put into the image at build time, and a
plugin version is changed by rebuilding the image. This page covers the two provisioning
sources, licensing, and — the part that catches every plugin — how a plugin that needs to
write to disk is accommodated under a read-only root filesystem.
Two sources
| Source | How |
|---|---|
Public (on wordpress.org) |
Declared as a Composer dependency via the WPackagist mirror and
installed in Stage 1 of the build. Versions are pinned in |
Private / commercial / third-party (not on wordpress.org) |
Supplied as an artefact (zip or tarball) and baked into the image in Stage 2 with a |
Public plugins (Composer)
Add the plugin to composer.json under the WPackagist repository and pin it via
composer.lock:
"repositories": [
{ "type": "composer", "url": "https://wpackagist.org", "only": ["wpackagist-plugin/*", "wpackagist-theme/*"] }
],
"require": {
"wpackagist-plugin/<slug>": "*"
}
The <slug> is the plugin’s wordpress.org slug, which equals its wp-content/plugins/
directory name. Confirm a plugin is actually hosted on wordpress.org before assuming it is
public — a 404 here means it is a private/third-party plugin and must be baked instead:
curl -s -o /dev/null -w "%{http_code}" https://api.wordpress.org/plugins/info/1.0/<slug>.json
|
Themes use a different endpoint
Both answer |
Private, commercial and third-party artefacts
These are not on WPackagist. The artefact is placed in a build-only, git-ignored directory and baked in:
# theme tarball whose root is wp-content/themes/<Name>
tar -xzf packages/<theme>.tar.gz --strip-components=2 -C web/app/themes wp-content/themes/<Name>
# premium plugin zip whose root is the plugin folder
unzip -q packages/<plugin>.zip -d web/app/plugins
# GPL plugin distributed only from a vendor git repo
git clone --depth 1 --branch <tag> <repo> /tmp/src && cp -r /tmp/src/<subdir> web/app/plugins/
Because the image build is where licensed artefacts are handled, the image is built in a trusted environment (not in shared CI, where large licensed zips cannot be stored as secrets). Only the Helm chart is published from CI.
Licensing
Commercial plugins are activated one of two ways, and neither puts a licence file on the read-only filesystem:
-
Online connect (e.g. UpdraftPlus, Divi) — the plugin code is baked in; activation is a one-time login/connect in the admin that registers the site against the account and stores a token in the database. Because it lives in the DB it survives every pod restart; you do it once after first deploy.
-
Licence key — a key entered in the admin, or injected as an environment variable from the Secret and applied during bootstrap.
Under the immutable model the plugin’s own auto-updater is irrelevant — updates come from rebuilding the image with a newer artefact, not from the admin.
|
A dev site is a second activation
An online-connect plugin activated on a dev site consumes a licence slot exactly as production does. Before standing one up, check whether the vendor sells the specific capability you need separately: UpdraftPlus' Migrator add-on is licensed apart from Premium and does not take a Premium slot, which is what makes the content lane affordable. Where no such split exists, prefer leaving the plugin inactive in dev over buying a slot for a site nobody visits. |
Writable paths under a read-only root filesystem
This is the rule that every plugin is measured against. The root filesystem is read-only, so a
plugin that writes anywhere under wp-content will fail unless that exact path is a mounted,
writable volume. Choosing the wrong kind of volume causes subtler failures, so the decision
matters:
| The path is… | Use | Because |
|---|---|---|
Persistent state the plugin owns as a self-contained store (e.g. backups) |
its own PVC |
It must survive restarts and must not be entangled with other data. Never make it a
|
Persistent, and genuinely part of the media tree |
the uploads PVC (optionally a |
It belongs with uploads and is not something the app deletes wholesale. |
Transient — a cache, or a working dir the app deletes and recreates |
an |
No need to persist, and an |
|
Two traps that cost real debugging time
A Never nest a plugin’s store inside another volume’s tree. Mounting a backup directory as a
|
Adding a plugin — checklist
Where a plugin is chosen and where it is declared are different places. On a site with a dev posture, try it there first: install it from wp-admin, confirm it does what you need and that it behaves under the writable-path rules above, and only then promote it. The harvest performs steps 1-3 for you — it detects anything installed in dev and not declared in the repo, classifies it against wordpress.org, and stages a private artefact out of the running site. Steps 4-6 remain judgement calls.
-
Determine the source:
curlthe wordpress.org API for the slug (plugins and themes use different endpoints — see the warning above). -
Public → add
wpackagist-plugin/<slug>tocomposer.json, runcomposer updateto pin it. Private → place the artefact in the build-onlypackages/dir and add theCOPY/extract step to the Dockerfile. -
Add the slug to the bootstrap activation list.
-
Identify any paths the plugin writes to under
wp-content; add a writable volume of the correct type (per the table above) to the Helm chart’s Deployment. -
If it needs a licence, decide connect-vs-key and where the secret lives.
-
Add a page to the plugin catalogue recording the above.