ClassicPress Common — Shared Application Configuration
ClassicPress_Common is the shared application layer for ClassicPress. It is
not deployed on its own; instead it supplies the ClassicPress-specific configuration
that both ClassicPress_GKE and
ClassicPress_CloudRun build on, so the two platform
variants behave identically where it matters. End users never configure this layer
directly — it has no deployment UI inputs of its own — but understanding what it
provides explains the defaults you see in the platform docs.
For the infrastructure that actually provisions and runs ClassicPress, see the platform guides (ClassicPress_GKE, ClassicPress_CloudRun) and the foundation guides (App_GKE, App_CloudRun, App_Common).
1. What this layer provides
| Area | Provided by ClassicPress_Common | Where it surfaces |
|---|---|---|
| Cryptographic secret | Generates CLASSICPRESS_SALT_SEED (64-char random seed) and stores it in Secret Manager | Injected automatically; the entrypoint derives all 8 WordPress-style auth keys/salts from it |
| Container image | Wraps the official classicpress/classicpress image with a thin custom entrypoint script; builds via Cloud Build | container_image output of the platform deployment |
| Database engine | Fixes Cloud SQL for MySQL 8.0 as the only supported engine (database_type = "MYSQL_8_0" hardcoded in config) | §Database in the platform guides |
| Database bootstrap | Defines the first-deploy job (db-init) that creates the database and user and grants privileges | initialization_jobs output |
| Object storage | Declares the Cloud Storage classicpress-uploads bucket | storage_buckets output |
| Core settings | Sets the baseline ClassicPress environment: DB port, table prefix, charset, conditional Redis env vars | Application behaviour in the platform guides |
| Health checks | Supplies the default startup/liveness probe defaults (TCP port 80 / HTTP /) | §Observability in the platform guides |
2. Cryptographic secrets in Secret Manager
One secret is generated automatically and stored in Secret Manager — it is never set in plain text and must never be changed after the first deployment:
CLASSICPRESS_SALT_SEED— a 64-character random string (no special characters) generated byrandom_password.classicpress_salt_seed. ClassicPress (a WordPress 4.9.x fork) needs 8 secretAUTH_KEY/SALTphrases (AUTH_KEY,SECURE_AUTH_KEY,LOGGED_IN_KEY,NONCE_KEY, and their matching_SALTvalues). Rather than store 8 separate secrets,ClassicPress_Commonstores this single seed and the entrypoint derives all 8 values deterministically (sha256(seed-<key-name>)) on every container start. Because/var/www/html(and thuswp-config.php) is not guaranteed to persist across container/pod recreation on every platform, deriving from one stable, secret-backed seed keeps sessions and signed cookies consistent across restarts and across a multi-instance fleet without relying on the image's own random-salt generation. Rotating this seed after first boot invalidates every signed cookie and logged-in session for all users immediately.
The secret's Secret Manager ID uses a single underscore, not a double underscore
(CLASSICPRESS_SALT_SEED, exposed as the output key of the same name) — GKE's
SecretSync CRD rejects targetKey values containing __, so this name is safe on
both platforms.
The database password is generated and managed separately by the foundation; its
secret name is reported in the platform deployment outputs (database_password_secret).
See App_Common for the shared secret and Workload Identity model.
Retrieve the secret after deployment:
# List secrets for this deployment (names include the resource prefix):
gcloud secrets list --project "$PROJECT" --filter="name~salt-seed"
# Read the secret version:
gcloud secrets versions access latest --secret=<secret-name> --project "$PROJECT"
An orphan-cleanup submodule (cleanup_orphaned_secrets) runs ahead of secret
creation to remove any stale secret-<prefix>-<app>-salt-seed left behind by a
previous partial deployment before the new one is created.
3. Database engine and bootstrap
ClassicPress requires MySQL 8.0; ClassicPress_Common's config hardcodes
database_type = "MYSQL_8_0" directly — the engine is fixed regardless of what an
Application module's own database_type variable is set to. On the first
deployment a one-shot job (db-init, script scripts/db-init.sh, image
mysql:8.0-debian) runs and idempotently:
- Waits for the Cloud SQL Auth Proxy Unix socket under
/cloudsql(up to 30 seconds) ifenable_cloudsql_volume = true; otherwise falls back to TCP viaDB_IP(preferred) or a non-socketDB_HOST, - Confirms
DB_PASSWORDandROOT_PASSWORDare present (injected by the foundation) and errors out clearly if either is missing, - Waits for TCP connectivity to port 3306 when not using a socket,
- Detects whether the local
mysqlclient supports--get-server-public-key(needed for MySQL 8'scaching_sha2_passworddefault auth plugin over a plain, proxy-terminated TCP connection) and adds the flag when available, - Creates (or updates the password of) the application user with
CREATE USER IF NOT EXISTS/ALTER USER ... IDENTIFIED BY, - Creates the application database with
CREATE DATABASE IF NOT EXISTS, - Grants
ALL PRIVILEGESon the database to the application user and flushes privileges, - Verifies the application user can actually connect and run
SELECT 1— this both catches password/grant mismatches early and warms MySQL'scaching_sha2_passwordserver-side auth cache for subsequent PHP connections, - Signals the Cloud SQL Auth Proxy sidecar to shut down gracefully (
POST /quitquitquit, falling back toSIGKILLafter a 30-second wait) so the job exits0rather than being retried underrestartPolicy: OnFailure.
There is no separate migration/schema job. Once db-init has provisioned the
empty database and user, ClassicPress creates its own schema and admin account
through its own first-run web installer (/wp-admin/install.php) — the same
pattern WordPress itself uses. The job is safe to re-run
(execute_on_apply = true, max_retries = 3, timeout_seconds = 600).
Inspect the database directly with:
gcloud sql connect <instance-name> --user=<db-user> --database=<db-name> --project "$PROJECT"
The instance, database, and user names are in the platform deployment outputs.
4. Container image and entrypoint
The custom image is a thin build FROM classicpress/classicpress:${CLASSICPRESS_VERSION}
(the base tag defaults to php8.3-apache; upstream tags are PHP-qualified, not
version-qualified, and the Application modules resolve application_version = "latest" to this pinned default via an app-specific CLASSICPRESS_VERSION build
arg — the Foundation's generic APP_VERSION build arg would otherwise silently
overwrite a same-named arg with the literal, non-existent tag "latest"). The
Dockerfile grafts a shell entrypoint (entrypoint.sh, copied to
/usr/local/bin/cp-entrypoint.sh) ahead of the upstream docker-entrypoint.sh:
- Translates
DB_*/DB_IPtoCLASSICPRESS_DB_*— ClassicPress'swp-config-docker.phpreads discreteCLASSICPRESS_DB_HOST/CLASSICPRESS_DB_NAME/CLASSICPRESS_DB_USER/CLASSICPRESS_DB_PASSWORDvariables, not the Laravel-styleDB_USERNAMEconvention some other PHP apps in this repo use. The Foundation injects the genericDB_HOST,DB_IP,DB_NAME,DB_USER,DB_PASSWORD; the entrypoint buildsCLASSICPRESS_DB_HOSTfrom${DB_IP:-DB_HOST}: a leading/is treated as a Cloud SQL socket directory (the entrypoint searches it for a socket file and emits the mysqlilocalhost:<socket>form), an empty value is left for the image's own default to surface a clear error, and anything else is treated as a TCP host and suffixed with:${DB_PORT:-3306}. MySQL over the Cloud SQL private-IP range needs no SSL (unlike Postgres), so this single code path works unmodified on both Cloud Run (DB_IP= the instance private IP) and GKE (DB_HOSToverridden to127.0.0.1, the cloud-sql-proxy sidecar). - Derives the 8 auth keys/salts — when
CLASSICPRESS_SALT_SEEDis present, computesCLASSICPRESS_AUTH_KEY,CLASSICPRESS_SECURE_AUTH_KEY,CLASSICPRESS_LOGGED_IN_KEY,CLASSICPRESS_NONCE_KEY, and their_SALTcounterparts assha256(seed-<key-name>), so every restart and every instance in a fleet agrees on the same values (see §2). - Hands off to the upstream entrypoint —
exec docker-entrypoint.sh "$@", which materialiseswp-config.phpfrom theCLASSICPRESS_*env vars, copies the ClassicPress application into/var/www/htmlif that directory is empty, and startsapache2-foregroundas PID 1 (the Dockerfile'sCMD).
Known limitation — the entrypoint never touches persistent storage.
ClassicPress_Common's Dockerfile and entrypoint.sh contain no reference to
/var/lib/classicpress, Filestore/NFS, or any Foundation-level NFS mount path —
confirmed by direct inspection of both files. The only two things the entrypoint
does are the DB_* aliasing above and salt derivation; it neither reads from nor
writes to any mount the Foundation might attach. Whether that matters depends
entirely on what the Foundation mounts over /var/www/html (the directory the
upstream docker-entrypoint.sh actually populates with the app, wp-content, and
uploads):
- On Cloud Run, no PVC-equivalent exists and the default
enable_nfs = truemounts Filestore at/var/lib/classicpress— a path this layer never reads or writes./var/www/htmlis therefore left on the container's own ephemeral filesystem, and withmin_instance_count = 0(the Cloud Run variant's default), every cold-started instance starts from an empty/var/www/html: uploaded media and any plugins/themes installed via wp-admin on a prior instance are lost, even though the Cloud SQL database (posts, pages, settings, and media metadata rows) is durable. This is confirmed, not theoretical — see ClassicPress_CloudRun §3 and §6. - On GKE, the Application module (not this Common layer) separately enables
stateful_pvc_enabled = true, which auto-selects aStatefulSetand mounts a block PVC atstateful_pvc_mount_path = /var/www/html— the exact directory the upstream entrypoint populates. That PVC is what actually persists the install across pod restarts on GKE; it is unrelated to, and not provided by, this Common module. GKE's ownenable_nfs = truedefault still mounts Filestore at/var/lib/classicpress, which remains unused by the entrypoint/Dockerfile for the same reason as on Cloud Run — see ClassicPress_GKE §3.
In short: this layer's NFS-unawareness is real and identical on both platforms: it
never wires /var/lib/classicpress (or any NFS mount) into where ClassicPress
actually stores its install. It is masked on GKE only because that variant's
Foundation-level StatefulSet PVC happens to be mounted at the correct path
(/var/www/html), not because ClassicPress_Common fixed anything.
5. Core application settings
ClassicPress_Common establishes the baseline ClassicPress environment so the
application comes up correctly on first boot:
- Database port —
DB_PORT = "3306", read byentrypoint.shto buildCLASSICPRESS_DB_HOSTinhost:portform. - Table prefix —
CLASSICPRESS_TABLE_PREFIX = "cp_"(the image default). - Character set —
CLASSICPRESS_DB_CHARSET = "utf8mb4". - Redis (conditional) — when
var.enable_redis = true, merges inREDIS_HOST(either the explicitvar.redis_hostor a literal$(REDIS_HOST)reference for the platform to resolve) andREDIS_PORT = var.redis_port.enable_redisis forwarded unchanged from the Application module — it is never gated on whetherredis_hostis set, so the Foundation's own NFS-VM Redis injection still fires whenredis_hostis left empty. - MySQL plugins —
enable_mysql_plugins = false,mysql_plugins = []; no MySQL server-side plugins are installed by default.
Platform-specific adjustments made in the Application modules (not this Common
layer, but built on top of its config output):
- Cloud Run forces
enable_cloudsql_volume = falsein itsmain.tfmerge (private-IP TCP by default, overriding whateverClassicPress_Commonmight otherwise pass through), soentrypoint.sh's TCP branch (DB_IP:DB_PORT) is what normally executes. - GKE merges in
DB_HOST = "127.0.0.1"on top ofClassicPress_Common'sconfig.environment_variables, and forcesenable_cloudsql_volume = true— the cloud-sql-proxy sidecar listens on loopback, soentrypoint.sh's TCP branch runs against127.0.0.1:3306instead.
php_memory_limit, upload_max_filesize, and post_max_size are declared as
variables and forwarded into the module call, but ClassicPress_Common's
config.environment_variables sets no corresponding PHP_* / upload-limit env var
for the upstream image to read — they currently have no observed effect on the
deployed container.
6. Health probe behaviour
ClassicPress_Common's variables.tf supplies the probe defaults that both
Application modules forward straight through (neither variant overrides them):
- Startup probe — TCP on the container port (80),
initial_delay_seconds = 30,period_seconds = 15,timeout_seconds = 10, and a generousfailure_threshold = 20(roughly 5 minutes of retries after the initial delay) — enough time for the upstreamdocker-entrypoint.shto populate an empty/var/www/htmlon first boot of a fresh instance or pod. - Liveness probe — HTTP
GET /,initial_delay_seconds = 300,period_seconds = 60,timeout_seconds = 60,failure_threshold = 3. Both a200(an already-installed site) and a302redirect to the first-run installer (a fresh, uninstalled site) count as healthy.
There is no CloudRun-vs-GKE difference at this layer — both platform variants use
identical probe defaults from ClassicPress_Common. (The GKE variant's own docs
note the same TCP/HTTP defaults; no per-platform override exists in either
main.tf.)
7. Object storage
A dedicated Cloud Storage bucket with name suffix classicpress-uploads is
declared in outputs.tf (location = var.region, force_destroy = true) and
provisioned by the foundation, which also grants the workload service account
access. It is not wired in as a gcs_volumes mount by either Application
module by default — add an entry to gcs_volumes (e.g. mounted at
/var/www/html/wp-content/uploads) if you want it to actually back media uploads.
gcloud storage buckets list --project "$PROJECT" --filter="name~classicpress-uploads"
For the ClassicPress-specific, user-facing configuration (variables by group, outputs, and how to explore each service from the Console and CLI), see the platform guides: ClassicPress_GKE and ClassicPress_CloudRun.