WordPress runs the open web, but the default "unzip it on a box" setup fights every cloud-native practice: core and plugins live in the document root, uploads pin you to one disk, and configuration hides in a file full of secrets. This recipe reshapes it into a 12-factor app. WordPress core, plugins and themes are Composer dependencies; only a public/ directory is web-served (config, vendor/ and tooling sit above it, unreachable over HTTP); every setting is read from the environment. Media is offloaded to S3 object storage and a persistent object cache lives on Valkey, so nothing durable touches the container disk and the app scales horizontally without going stale.
Six environments cover the whole lifecycle: an AI Agent dev + stage pair for agent-driven development, a single Remote (CDE) cloud workstation, a stores-only Local setup for developing over the VPN, a single-node Stage, and production on shared (Small) or dedicated, highly-available (HA) hardware behind zero-downtime rolling deploys.
Features
- Composer-managed WordPress — core, plugins and themes are dependencies; no in-dashboard file writes (
DISALLOW_FILE_MODS), so deploys stay reproducible. - Isolated web root — only
public/is served;wp-config.phpandvendor/live one level up, physically unreachable, not just "denied". - Redis object cache — persistent cache on Valkey (phpredis); the
object-cache.phpdrop-in is baked into the build artifact, so every container ships with it and just connects to the managed Valkey — graceful if it's momentarily unreachable. - S3 media — uploads go straight to object storage via
humanmade/s3-uploads, path-style for MinIO; the container filesystem stays disposable. - Zero-downtime deploys — a readiness check gates traffic on a static core asset; a health check restarts unhealthy containers.
- Real cron — WordPress pseudo-cron is disabled; a platform cron runs
wp cron event runevery 5 minutes on one container. - Hardened & tuned — security headers,
xmlrpc.phpblocked, no PHP execution under uploads, production OPcache, WP-CLI pinned by checksum. - SMTP-ready email — point
WORDPRESS_SMTP_HOSTat your relay to route mail through it; left unset, WordPress uses PHP's defaultmail()transport.
A production-ready stack sized for moderate traffic: single-node MariaDB, Valkey and a 10 GB media bucket, with the app on shared CPU scaling 1 → 2 containers behind readiness-gated rolling deploys. OPcache is timestamp-frozen and WP_DEBUG is off. The cheapest way to run a real, public WordPress site; not redundant.
Knowledge Base
Architecture
The app service (php-nginx) serves only public/; WordPress core is in public/wp, content in public/wp-content, and the real wp-config.php sits at the repo root — above the web root, so it can never be served. State is externalized: db (MariaDB) holds content, storage (object storage) holds media via humanmade/s3-uploads, and cache (Valkey) holds the persistent object cache via redis-cache. The object-cache.php drop-in is baked into the build artifact — copied from the version-matched redis-cache plugin during the build — so it ships inside every immutable container and connects to the managed Valkey on boot, with no runtime install step.
Compared to Bedrock
If you already run Bedrock, the front half looks familiar — Composer-managed core, an isolated web root, configuration from the environment; those are sensible WordPress practices and this recipe applies them too. Where it goes further is everything below that line, wired to the platform rather than left for you to assemble: managed MariaDB, Valkey and object storage; a build-baked Redis object cache; S3 media; health- and readiness-gated zero-downtime deploys; real cron; and tuned OPcache — provisioned and running as one project, across six environments from an AI-agent workstation up to an HA cluster.
Environment variables
- Wired for you (in the app's
zerops.yaml):WORDPRESS_DB_*←db,WORDPRESS_STORAGE_*←storage,WORDPRESS_REDIS_*←cache,WORDPRESS_URL← the subdomain. - Generated secrets (in
import.yaml): the eight auth keys/salts andWORDPRESS_ADMIN_PASSWORD. - Yours to set:
WORDPRESS_ADMIN_EMAIL, andSMTP_HOST_OVERRIDE/SMTP_PORT_OVERRIDEto route mail through a real SMTP relay. - Booleans are coerced —
WORDPRESS_DEBUGetc. are parsed withFILTER_VALIDATE_BOOLEAN, so the string"false"is actually false (in plain WordPress it would be truthy and silently turn debug ON).
Troubleshooting
- Redis "connection timed out" right after deploy — the drop-in degrades gracefully (
WP_REDIS_GRACEFUL); the container serves uncached until Valkey warms up, then reconnects. Persistent failure → check thecacheservice andWORDPRESS_REDIS_PASSWORD(Valkey auth is mandatory on Zerops). - Media 404s — media URLs point at the
storageendpoint, not the app domain; confirm the bucket policy ispublic-objects-readandmu-plugins/s3.phpsetuse_path_style_endpoint. - Uploads rejected at 50 MB — that's the L7 balancer cap on the
*.zerops.appsubdomain, not PHP; use a custom domain for larger files.