WordPress on Kubernetes with Kapitan
WordPress 7.1.4 on PHP 8.5 and Apache with its wp-content on a claim, designed in kadet-hub on the Docker Official Image and rendered by the kadet-hub kubernetes generator 1.14.0. The database is the included recipe mariadb.
- Latest version:
1.0.1, published - Source: container image
docker.io/library/wordpress:7.1.4-php8.5-apache@sha256:41666f6d61dd038f87ceacd6323685bd9818a3a9b3d80d98af3ce9797c403dce, recipe designed in kadet-hub - Proof level:
deploys, deployed on kind and smoke tested by the kadet-hub/recipes CI renderspassed in the release run of with check 0.18.0, kind v0.33.0- Output capabilities: none
- Inventory backends:
omegaconf - Generators: kubernetes 1.14.0
sha256:787129e1eaed6b9bc868fe4c4aec58d40de6a89fc3cc08424a4cb5822b855080 - Image security: grade F 2510 findings, one per advisory and package, in the 1 image of version 1.0.1: CRITICAL 2, HIGH 206, MEDIUM 1075, LOW 1135, UNKNOWN 92. Scanned 2026-11-07 11:47 UTC. Full report
The grade rates the upstream images the recipe pins, not the recipe:docker.io/library/wordpress.
What it deploys
recipes.wordpress(init.yml)recipes.wordpress.wordpress(wordpress.yml):wordpress
Required parameters
The eight keys and salts of wp-config.php must be set:
applications.wordpress.auth_key, applications.wordpress.secure_auth_key,
applications.wordpress.logged_in_key, applications.wordpress.nonce_key,
applications.wordpress.auth_salt, applications.wordpress.secure_auth_salt,
applications.wordpress.logged_in_salt and
applications.wordpress.nonce_salt. Each is a long random string, such as the values that
https://api.wordpress.org/secret-key/1.1/salt/ generates. WordPress
signs its login cookies with them, so a change logs every user out. The
included recipe mariadb requires applications.mariadb.password, the
password with which WordPress connects as the user app, and
applications.mariadb.root_password. Set all of them as refs with the
backend of your choice. Without them the compile stops with
Interpolation key 'applications.wordpress.auth_key' not found.
The recipe does not install WordPress. The first visitor of
/wp-admin/install.php names the site and creates the admin account, so
install it before the Service is reachable by others.
Include
A target includes the recipe and its options as classes and can override anything the recipe sets:
classes:
- recipes.wordpress
parameters:
namespace: wordpress
kapitan:
vars:
target: wordpress
applications:
mariadb:
password: <ref>
root_password: <ref>
wordpress:
auth_key: <ref>
secure_auth_key: <ref>
logged_in_key: <ref>
nonce_key: <ref>
auth_salt: <ref>
secure_auth_salt: <ref>
logged_in_salt: <ref>
nonce_salt: <ref>
Fetching it
The quickstart goes through these steps on a kind cluster with the uptime-kuma recipe, from installing Kapitan to the running app.
A separate fetch inventory pins the recipe by version and digest. This is fetch/targets/recipes.yml for version 1.0.1:
parameters:
kapitan:
dependencies:
- type: oci
source: ghcr.io/kadet-hub/recipes/wordpress:1.0.1@sha256:4535357124d5173e6b053d7680d53920170ca69b618c2941e054cf6c87230799
output_path: inventory/classes/recipes/wordpress
Version 1.0.1 includes mariadb 1.0.1. The consumer workflow adds the included recipes at the digests the index lists. Kapitan's --fetch reads no index, so for a local compile pull them by digest first:
tmp=$(mktemp -d)
oras pull -o "$tmp" ghcr.io/kadet-hub/recipes/mariadb@sha256:0ab0c69a1a1efaeb00354a4e44885cf87cd1084bddb36ea767a77d23151b5662
mkdir -p inventory/classes/recipes/mariadb
tar -xf "$tmp/mariadb.tar" -C inventory/classes/recipes/mariadb --no-same-owner
The first command fetches the recipe into inventory/classes/recipes/wordpress, the second the generators and compiles:
kapitan compile --inventory-path fetch --fetch --output-path .
kapitan compile --fetch
The first command exits with status 1 because the fetch target compiles nothing. Kapitan's --fetch checks neither the digest nor the attestation. This verifies the attestation of the recipe and of the recipes it includes:
gh attestation verify \
oci://ghcr.io/kadet-hub/recipes/wordpress@sha256:4535357124d5173e6b053d7680d53920170ca69b618c2941e054cf6c87230799 \
--owner kadet-hub \
--cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main
gh attestation verify \
oci://ghcr.io/kadet-hub/recipes/mariadb@sha256:0ab0c69a1a1efaeb00354a4e44885cf87cd1084bddb36ea767a77d23151b5662 \
--owner kadet-hub \
--cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main
The attested index must also list each digest under its recipe with the pinned version, and not as yanked. Verifying by hand shows how to read it.
Settings
| Setting | Inventory |
|---|---|
| WordPress and PHP version | components.wordpress.image, tag and digest |
wp-content volume size, 8Gi |
components.wordpress.volume_claims.data.spec.resources.requests.storage |
| resources | components.wordpress.resources |
further wp-config.php settings, such as WP_HOME |
components.wordpress.env.WORDPRESS_CONFIG_EXTRA, PHP that wp-config.php runs |
table prefix, wp_ |
components.wordpress.env.WORDPRESS_TABLE_PREFIX |
| database, its user and both passwords | the settings of mariadb |
The tag names the PHP version, and Renovate keeps it. Pick another PHP
version with a tag such as 7.1.4-php8.4-apache when a plugin needs it.
The core files come from the image at every start of the pod, and
wp-config.php from the environment. Only wp-content, with uploads,
plugins and themes, lives on the claim. WordPress’s automatic core updates
are on, as in the image, but they write into the container and are lost
with the pod. A new WordPress version comes with the image tag. After it,
open /wp-admin/ once if WordPress asks to update its database.
The site URL is the URL the installation was opened at. Exposing the
Service, such as with an Ingress or a route, is left to the target.
wp-config.php turns on HTTPS when a proxy sends
X-Forwarded-Proto: https.
The pod runs as the image’s user www-data, UID 33, and passes Pod
Security restricted. Apache binds port 80 as that user because the pod
sets the sysctl net.ipv4.ip_unprivileged_port_start to 0, which
restricted allows. As that user the image’s entrypoint stops on a
wp-content that another user owns, such as the root of a new volume. The
init container wp-content therefore creates the directory wp-content
on the claim as www-data, and the pod mounts it from there. WordPress
connects to MariaDB without TLS.
The readiness probe gets /wp-login.php every 10 seconds. It fails while
WordPress cannot reach its database. There is no liveness probe, which
would restart WordPress during a database outage.
Proof
check wordpress --level deploys with tests/target.yml on kind for each
Kubernetes minor CI tests. The image has no Kubernetes objects to compare
with, so the recipe is proven by deploying it (RCP-44). tests/smoke
checks that the pods of WordPress and MariaDB pass Pod Security
restricted. From a pod of the WordPress image, through the Service
wordpress on port 80, it installs WordPress through
/wp-admin/install.php, requires the image tag’s version on the front
page, gets no session for a wrong password and logs in. Through the REST
API with the login’s cookie it publishes a post, which it reads without
the session on its permalink, and uploads a PNG, which it fetches byte for
byte. It deletes the pod wordpress-0 and reads the post and the PNG again
once the new pod is ready, and reaches /wp/v2/users/me with the same
session.
Versions
1.0.1, :sha256:4535357124d5173e6b053d7680d53920170ca69b618c2941e054cf6c872307991.0.0, :sha256:5dea60e780883fd87aa77f0f2417e2c74aeaa5068d82e43ffae04496e4715590
The recipe's files are in kadet-hub/recipes. All recipes