kadet-hub logokadet-hub

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.

What it deploys

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

The recipe's files are in kadet-hub/recipes. All recipes