Provisioning a WordPress dev site
The design — the two postures, the seed overlay, the three promotion lanes — is Dev site and promotion. This runbook is the one-time provisioning that has to happen before any of it works, in the order it has to happen.
Worked here against tourdeworcester.co.za; the shape is the same for any event site.
Preconditions
| Why it matters | |
|---|---|
A |
|
The chart version supporting |
0.2.0 or later. Point only the dev Application at it; moving production’s |
A namespace that is not production’s |
|
1. Database
Its own schema and user. Never production’s — see the warning at the end of this page.
CREATE DATABASE wp_tourdeworcester_dev
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;
CREATE USER 'tdw_dev'@'%' IDENTIFIED BY '<set in step 2>';
GRANT ALL PRIVILEGES ON wp_tourdeworcester_dev.* TO 'tdw_dev'@'%';
Leave the password until step 2 — it is generated there, not chosen here.
|
This is the shared production MySQL cluster. Keep the dev pod small and do not raise its
replica count: a |
2. Secrets
scripts/create-dev-secret.sh generates everything on your machine and writes it straight
into the cluster. No secret is printed, so none reaches shell history, a terminal scrollback
or an agent transcript.
scripts/create-dev-secret.sh --namespace wordpress-dev --name wp-tourdeworcester-dev
It creates three Secrets and copies a fourth:
| Secret | Contents |
|---|---|
|
|
|
The |
|
The same username and password in readable form. It exists precisely because the hash above is one-way: without it the password would be unrecoverable the moment the terminal closed. |
|
The image pull secret, copied from the production namespace. Not generated — copied, because it is the same registry credential. |
|
The pull secret is the easiest one to forget
A fresh namespace has none, and the symptom is |
What the basic-auth secret is protecting against
Three things, and the third is the one that changes over the life of the site:
-
Discovery. A dev copy of a live store on a public hostname is findable, and a visitor cannot tell it from the real one.
-
Indexing.
blog_public = 0and theX-Robots-Tagheader ask crawlers not to index it; basic auth means a crawler never gets a page to consider in the first place. -
Personal data. The moment a production content refresh is pulled in, the site holds real customers, real orders and real addresses. That is when edge auth stops being tidiness and starts being the control that matters — paired with the scrub, which handles what is inside the site.
|
Basic auth covers the whole host, including machine callers
A server-to-server call has no credentials to offer and gets a 401. That silently breaks the event payment plugin’s inbound order-create endpoint — which is most of the reason for running this site in the cluster rather than on a laptop.
Keep the list as narrow as the caller needs — anything on it is reachable by anyone who knows
the URL. Never put |
Re-running is safe: without --rotate it leaves existing secrets alone, because regenerating
the salts logs every session out and regenerating DB_PASSWORD breaks the site until the
grant is changed to match.
Then set the database grant to the generated password:
kubectl -n wordpress-dev get secret wp-tourdeworcester-dev \
-o jsonpath='{.data.DB_PASSWORD}' | base64 -d; echo
ALTER USER 'tdw_dev'@'%' IDENTIFIED BY '<the value above>';
FLUSH PRIVILEGES;
Retrieving the logins later:
# WordPress admin
kubectl -n wordpress-dev get secret wp-tourdeworcester-dev \
-o jsonpath='{.data.WP_ADMIN_PASSWORD}' | base64 -d; echo
# Ingress basic-auth
kubectl -n wordpress-dev get secret wp-tourdeworcester-dev-basicauth-plain \
-o jsonpath='{.data.password}' | base64 -d; echo
3. DNS and TLS
Put the dev host in a zone external-dns owns, and the record and certificate both appear
without intervention. For Tour de Worcester that is tdw-dev.myriadevents.co.za, not a
subdomain of tourdeworcester.co.za.
That choice is not cosmetic. The tourdeworcester.co.za zone is manually managed, so a host
there needs a hand-created A record before the first sync — otherwise cert-manager’s
HTTP-01 challenge cannot resolve, the certificate never issues, and the site answers on plain
HTTP or not at all. Picking an external-dns zone removes the step and the failure mode with
it.
4. Sync
Add the Application to the ops repo and sync it. Then watch the bootstrap, which is where provisioning mistakes actually surface:
kubectl -n wordpress-dev logs deploy/wp-tourdeworcester-dev -c bootstrap
| What you see | What it means |
|---|---|
|
Correct. The seed overlay found |
|
The image was not built with |
|
The grant in step 2 does not match the Secret, or the schema does not exist. |
|
The schema was emptied, or the release was pointed at a different database. |
|
First start, or the declared config changed. Both are expected; see below. |
5. Mail
Nothing to provision. The dev site sends to GreenMail in the event-dev namespace
(greenmail.event-dev.svc.cluster.local:25), which has auth and TLS disabled, so there is no
credential and nothing in the Secret.
Read what the site sent in SnappyMail at https://event-mail-dev.idealogic.co.za.
|
SnappyMail is configured per sender domain, and its list covers |
Confirm the transport works end to end:
kubectl -n wordpress-dev exec deploy/wp-tourdeworcester-dev -c wordpress -- \
wp --path=/var/www/html/web/wp eval \
'var_dump(wp_mail("[email protected]", "dev site check", "hello"));'
false means no transport. true with nothing in SnappyMail usually means the sender address
is not mapped — see FluentSMTP, which also
covers the two other delivery routes (Microsoft 365 Graph and Amazon SES) and when to choose
them over plain SMTP.
6. Post-sync, in wp-admin
-
UpdraftPlus — activate the Migrator licence, and only that. Do not perform the Premium connect on a dev site: that consumes a Premium site slot, whereas the Migrator add-on’s licence explicitly does not. See wordpress-hosting:plugins/updraftplus.adoc#content-lane.
-
PayGate — test mode. Never live credentials on a site that is not taking real money.
-
Confirm the payment plugin points at stage, not production:
kubectl -n wordpress-dev exec deploy/wp-tourdeworcester-dev -c wordpress -- \ wp --path=/var/www/html/web/wp option list --search='epa_*'epa_admin_api_urlis a base URL and carries no/apisuffix — the plugin appends the path itself, so a suffix produces/api/api/…and a 404.
7. Verify the posture actually took
Three checks, each catching a different silent failure:
# Plugins are writable — this is what makes it a dev site
kubectl -n wordpress-dev exec deploy/wp-tourdeworcester-dev -c wordpress -- \
sh -c 'touch /var/www/html/web/app/plugins/.writetest && echo writable && rm /var/www/html/web/app/plugins/.writetest'
# Core is NOT writable — this is what keeps composer.lock authoritative
kubectl -n wordpress-dev exec deploy/wp-tourdeworcester-dev -c wordpress -- \
sh -c 'touch /var/www/html/web/wp/.writetest 2>&1 || echo "core read-only, correct"'
# The site is not crawlable and not publicly readable
curl -sI https://tdw-dev.myriadevents.co.za/ | head -1 # expect 401
curl -sI -u <user>:<pass> https://tdw-dev.myriadevents.co.za/ | grep -i x-robots-tag
Finally, confirm a restart does not wipe an unpromoted change: install anything from wp-admin,
kubectl rollout restart, and check it survived. If it did not, the plugins PVC is not mounted
and you are looking at an emptyDir — check persistence.enabled.
|
The failure this whole runbook is shaped around
The archived A dev site gets its own schema. Not production’s, and not another environment’s. |
Related
-
Dev site and promotion — the design, and how changes made here get back into production.
-
ArgoCD Deployment Patterns — the general manifest and secret conventions this follows.
-
Immutable WordPress architecture — the production posture.