From 5608106f7f8d20d7a861ca71f2e6e53b5ad8ef3e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 07:36:01 +0000 Subject: [PATCH 01/29] chore: add OSS license and security policy Co-authored-by: Manuel H. --- LICENSE | 21 ++++++++++ README.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++- SECURITY.md | 33 ++++++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 LICENSE create mode 100644 SECURITY.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..97f7922 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 The Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index a18f153..07bb47b 100644 --- a/README.md +++ b/README.md @@ -1 +1,110 @@ -# vd-wordpress-boilerplate +# Modern Cloud-Native WordPress Boilerplate (Bedrock) + +A production-minded WordPress boilerplate based on **[Roots Bedrock](https://roots.io/bedrock/)**, designed for: + +- Local development with **Docker Compose** (profiles for optional services) +- Hardened, reproducible **container builds** (multi-stage) +- **Kubernetes-first** deployment via **Helm** +- Supply-chain and security gates in **GitHub Actions** + +> This repo is intentionally generic and safe to publish: no internal domains, no secrets, and no private themes/plugins/submodules. + +--- + +## Quickstart (Local Development) + +### Prerequisites + +- Docker + Docker Compose v2 +- `make` + +### Run it + +```bash +cp .env.example .env +make up +``` + +Then open: + +- http://localhost:8080 + +To stop: + +```bash +make down +``` + +### Optional profiles (dev conveniences) + +```bash +# Adds MailHog (SMTP capture UI at http://localhost:8025) +make up-mail + +# Adds phpMyAdmin (http://localhost:8081) +make up-dbadmin + +# Adds metrics exporters (Prometheus scrape targets) +make up-observability +``` + +--- + +## Production Images (Hardened) + +This boilerplate provides: + +- A PHP-FPM image (Bedrock + WordPress via Composer) +- A web image (Nginx, non-root) +- Secure-by-default runtime posture (drop caps, no privilege escalation, read-only root filesystem where possible) + +Images are intended to be built in CI and deployed **by digest** (build once, deploy everywhere). + +--- + +## Deploy to Kubernetes (Helm) + +Helm chart: `helm/wp-boilerplate` + +High-level steps: + +1. Push images to a registry (GHCR workflow included). +2. Create Kubernetes Secrets for database credentials and WordPress salts/keys. +3. Install the chart and pin images by digest. + +Example: + +```bash +helm upgrade --install wp helm/wp-boilerplate \ + --namespace wp --create-namespace \ + --set image.php.repository=ghcr.io/OWNER/REPO-php \ + --set image.web.repository=ghcr.io/OWNER/REPO-nginx \ + --set image.php.digest=sha256:... \ + --set image.web.digest=sha256:... +``` + +### Uploads storage strategy + +Preferred (cloud-native): use an S3-compatible uploads plugin (no shared PVC required). + +Alternative (simple clusters): enable the chart's `uploads.persistence` option to mount a PVC. + +--- + +## WP-Cron (Production) + +In production, you should disable the built-in pseudo-cron and run a real scheduler: + +- Set `DISABLE_WP_CRON=true` +- Use a Kubernetes `CronJob` (template included) or an external scheduler to trigger cron processing + +--- + +## License + +MIT. See [LICENSE](./LICENSE). + +## Security + +See [SECURITY.md](./SECURITY.md). + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b0fe116 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,33 @@ +# Security Policy + +## Supported Versions + +This repository is a boilerplate. Security updates are provided on a best-effort basis on the default branch. + +If you are using this boilerplate for a real deployment, you are responsible for: + +- Updating WordPress core (via Composer in Bedrock) +- Updating PHP / base container images +- Applying security updates to plugins/themes you add + +## Reporting a Vulnerability + +Please **do not** open a public GitHub issue for security reports. + +Preferred: use **GitHub Security Advisories** for private disclosure. + +1. Go to the repository page on GitHub +2. Click **Security** +3. Click **Report a vulnerability** + +If GitHub private reporting is not available, you may contact the maintainers by opening an issue asking for a private contact channel (do not include sensitive details). + +## Disclosure Policy + +We aim to: + +- Acknowledge receipt within **7 days** +- Provide a remediation plan or fix within **30 days** (when feasible) + +Timelines may vary depending on severity and available maintainer bandwidth. + From 5e411cbd0033acda32c33d21321ae631e5210222 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 07:40:24 +0000 Subject: [PATCH 02/29] feat: add Bedrock application baseline Co-authored-by: Manuel H. --- .editorconfig | 16 + .env.example | 48 + .gitattributes | 4 + .gitignore | 39 + composer.json | 72 ++ composer.lock | 1124 ++++++++++++++++++++ config/application.php | 184 ++++ config/environments/development.php | 23 + config/environments/production.php | 11 + config/environments/staging.php | 16 + web/app/mu-plugins/bedrock-autoloader.php | 17 + web/app/plugins/.gitkeep | 1 + web/app/themes/starter-theme/README.md | 7 + web/app/themes/starter-theme/footer.php | 14 + web/app/themes/starter-theme/functions.php | 13 + web/app/themes/starter-theme/header.php | 19 + web/app/themes/starter-theme/index.php | 25 + web/app/themes/starter-theme/style.css | 24 + web/app/uploads/.gitkeep | 1 + web/index.php | 8 + web/wp-config.php | 12 + 21 files changed, 1678 insertions(+) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config/application.php create mode 100644 config/environments/development.php create mode 100644 config/environments/production.php create mode 100644 config/environments/staging.php create mode 100644 web/app/mu-plugins/bedrock-autoloader.php create mode 100644 web/app/plugins/.gitkeep create mode 100644 web/app/themes/starter-theme/README.md create mode 100644 web/app/themes/starter-theme/footer.php create mode 100644 web/app/themes/starter-theme/functions.php create mode 100644 web/app/themes/starter-theme/header.php create mode 100644 web/app/themes/starter-theme/index.php create mode 100644 web/app/themes/starter-theme/style.css create mode 100644 web/app/uploads/.gitkeep create mode 100644 web/index.php create mode 100644 web/wp-config.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..18f31e6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.php] +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..43bf9f9 --- /dev/null +++ b/.env.example @@ -0,0 +1,48 @@ +# ----------------------------------------------------------------------------- +# Bedrock / WordPress configuration (safe example) +# ----------------------------------------------------------------------------- + +WP_ENV=development +WP_HOME=http://localhost:8080 +WP_SITEURL=${WP_HOME}/wp + +# ----------------------------------------------------------------------------- +# Database (local dev defaults) +# ----------------------------------------------------------------------------- + +DB_NAME=wordpress +DB_USER=wordpress +DB_PASSWORD=wordpress +DB_HOST=db +DB_PREFIX=wp_ + +# ----------------------------------------------------------------------------- +# Authentication Keys and Salts +# Generate new values for real environments: +# https://roots.io/salts.html +# ----------------------------------------------------------------------------- + +AUTH_KEY='generateme' +SECURE_AUTH_KEY='generateme' +LOGGED_IN_KEY='generateme' +NONCE_KEY='generateme' +AUTH_SALT='generateme' +SECURE_AUTH_SALT='generateme' +LOGGED_IN_SALT='generateme' +NONCE_SALT='generateme' + +# ----------------------------------------------------------------------------- +# WordPress hardening / behavior +# ----------------------------------------------------------------------------- + +DISABLE_WP_CRON=false +WP_POST_REVISIONS=25 + +# ----------------------------------------------------------------------------- +# Redis object cache (optional; requires a Redis cache plugin) +# ----------------------------------------------------------------------------- + +WP_CACHE=false +WP_REDIS_HOST=redis +WP_REDIS_PORT=6379 + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1d0cd96 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..409ef05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Application +web/app/plugins/* +!web/app/plugins/.gitkeep +web/app/mu-plugins/*/ +web/app/themes/* +!web/app/themes/starter-theme/ +!web/app/themes/starter-theme/** +web/app/upgrade +web/app/uploads/* +!web/app/uploads/.gitkeep +web/app/cache/* + +# WordPress (managed by Composer) +web/wp +web/.htaccess + +# Logs +*.log + +# Dotenv +.env +.env.* +!.env.example + +# Composer +/vendor +auth.json + +# WP-CLI +wp-cli.local.yml + +# Node (if you add a frontend toolchain later) +node_modules + +# OS / IDE +.DS_Store +.idea +.vscode + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..681e5af --- /dev/null +++ b/composer.json @@ -0,0 +1,72 @@ +{ + "name": "cloud-native/wordpress-bedrock-boilerplate", + "type": "project", + "license": "MIT", + "description": "Modern cloud-native WordPress boilerplate built on Roots Bedrock", + "keywords": [ + "bedrock", + "composer", + "wordpress", + "docker", + "kubernetes", + "helm" + ], + "repositories": [ + { + "name": "wpackagist", + "type": "composer", + "url": "https://wpackagist.org", + "only": [ + "wpackagist-plugin/*", + "wpackagist-theme/*" + ] + } + ], + "require": { + "php": ">=8.2", + "composer/installers": "^2.2", + "oscarotero/env": "^2.1", + "roots/bedrock-autoloader": "^1.0", + "roots/bedrock-disallow-indexing": "^2.0", + "roots/wordpress": "6.9.1", + "roots/wp-config": "1.0.0", + "vlucas/phpdotenv": "^5.6" + }, + "require-dev": { + "laravel/pint": "^1.18" + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "allow-plugins": { + "composer/installers": true, + "roots/wordpress-core-installer": true + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "extra": { + "installer-paths": { + "web/app/mu-plugins/{$name}/": [ + "type:wordpress-muplugin" + ], + "web/app/plugins/{$name}/": [ + "type:wordpress-plugin" + ], + "web/app/themes/{$name}/": [ + "type:wordpress-theme" + ] + }, + "wordpress-install-dir": "web/wp" + }, + "scripts": { + "lint": "pint --test", + "lint:fix": "pint", + "security:audit": "composer audit" + }, + "suggest": { + "wpackagist-plugin/redis-cache": "Redis-backed object cache plugin (enable WP_CACHE + WP_REDIS_* env vars)", + "humanmade/s3-uploads": "S3-compatible uploads offload (recommended for Kubernetes)" + } +} + diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..c400dd1 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1124 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "a863691fb8e2dfe0e7ef69cae6c32f77", + "packages": [ + { + "name": "composer/installers", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/composer/installers.git", + "reference": "12fb2dfe5e16183de69e784a7b84046c43d97e8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/installers/zipball/12fb2dfe5e16183de69e784a7b84046c43d97e8e", + "reference": "12fb2dfe5e16183de69e784a7b84046c43d97e8e", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "composer/composer": "^1.10.27 || ^2.7", + "composer/semver": "^1.7.2 || ^3.4.0", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-phpunit": "^1", + "symfony/phpunit-bridge": "^7.1.1", + "symfony/process": "^5 || ^6 || ^7" + }, + "type": "composer-plugin", + "extra": { + "class": "Composer\\Installers\\Plugin", + "branch-alias": { + "dev-main": "2.x-dev" + }, + "plugin-modifies-install-path": true + }, + "autoload": { + "psr-4": { + "Composer\\Installers\\": "src/Composer/Installers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kyle Robinson Young", + "email": "kyle@dontkry.com", + "homepage": "https://github.com/shama" + } + ], + "description": "A multi-framework Composer library installer", + "homepage": "https://composer.github.io/installers/", + "keywords": [ + "Dolibarr", + "Eliasis", + "Hurad", + "ImageCMS", + "Kanboard", + "Lan Management System", + "MODX Evo", + "MantisBT", + "Mautic", + "Maya", + "OXID", + "Plentymarkets", + "Porto", + "RadPHP", + "SMF", + "Starbug", + "Thelia", + "Whmcs", + "WolfCMS", + "agl", + "annotatecms", + "attogram", + "bitrix", + "cakephp", + "chef", + "cockpit", + "codeigniter", + "concrete5", + "concreteCMS", + "croogo", + "dokuwiki", + "drupal", + "eZ Platform", + "elgg", + "expressionengine", + "fuelphp", + "grav", + "installer", + "itop", + "known", + "kohana", + "laravel", + "lavalite", + "lithium", + "magento", + "majima", + "mako", + "matomo", + "mediawiki", + "miaoxing", + "modulework", + "modx", + "moodle", + "osclass", + "pantheon", + "phpbb", + "piwik", + "ppi", + "processwire", + "puppet", + "pxcms", + "reindex", + "roundcube", + "shopware", + "silverstripe", + "sydes", + "sylius", + "tastyigniter", + "wordpress", + "yawik", + "zend", + "zikula" + ], + "support": { + "issues": "https://github.com/composer/installers/issues", + "source": "https://github.com/composer/installers/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-06-24T20:46:46+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "oscarotero/env", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/oscarotero/env.git", + "reference": "9f7d85cc6890f06a65bad4fe0077c070d596e4a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/oscarotero/env/zipball/9f7d85cc6890f06a65bad4fe0077c070d596e4a4", + "reference": "9f7d85cc6890f06a65bad4fe0077c070d596e4a4", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "phpunit/phpunit": ">=7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/env_function.php" + ], + "psr-4": { + "Env\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oscar Otero", + "email": "oom@oscarotero.com", + "homepage": "http://oscarotero.com", + "role": "Developer" + } + ], + "description": "Simple library to consume environment variables", + "homepage": "https://github.com/oscarotero/env", + "keywords": [ + "env" + ], + "support": { + "email": "oom@oscarotero.com", + "issues": "https://github.com/oscarotero/env/issues", + "source": "https://github.com/oscarotero/env/tree/v2.1.1" + }, + "time": "2024-12-03T01:02:28+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "roots/bedrock-autoloader", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/roots/bedrock-autoloader.git", + "reference": "f508348a3365ab5ce7e045f5fd4ee9f0a30dd70f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roots/bedrock-autoloader/zipball/f508348a3365ab5ce7e045f5fd4ee9f0a30dd70f", + "reference": "f508348a3365ab5ce7e045f5fd4ee9f0a30dd70f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "10up/wp_mock": "^0.4.2", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Roots\\Bedrock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nick Fox", + "email": "nick@foxaii.com", + "homepage": "https://github.com/foxaii" + }, + { + "name": "Scott Walkinshaw", + "email": "scott.walkinshaw@gmail.com", + "homepage": "https://github.com/swalkinshaw" + }, + { + "name": "Austin Pray", + "email": "austin@austinpray.com", + "homepage": "https://github.com/austinpray" + } + ], + "description": "An autoloader that enables standard plugins to be required just like must-use plugins", + "keywords": [ + "autoloader", + "bedrock", + "mu-plugin", + "must-use", + "plugin", + "wordpress" + ], + "support": { + "forum": "https://discourse.roots.io/", + "issues": "https://github.com/roots/bedrock-autoloader/issues", + "source": "https://github.com/roots/bedrock-autoloader/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/roots", + "type": "github" + }, + { + "url": "https://www.patreon.com/rootsdev", + "type": "patreon" + } + ], + "time": "2020-12-04T15:59:12+00:00" + }, + { + "name": "roots/bedrock-disallow-indexing", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/roots/bedrock-disallow-indexing.git", + "reference": "6c28192e17cb9e02a5c0c99691a18552b85e1615" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roots/bedrock-disallow-indexing/zipball/6c28192e17cb9e02a5c0c99691a18552b85e1615", + "reference": "6c28192e17cb9e02a5c0c99691a18552b85e1615", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "wordpress-muplugin", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Word", + "email": "ben@benword.com", + "homepage": "https://github.com/retlehs" + }, + { + "name": "Scott Walkinshaw", + "email": "scott.walkinshaw@gmail.com", + "homepage": "https://github.com/swalkinshaw" + }, + { + "name": "QWp6t", + "email": "hi@qwp6t.me", + "homepage": "https://github.com/qwp6t" + } + ], + "description": "Disallow indexing of your site on non-production environments", + "keywords": [ + "wordpress" + ], + "support": { + "forum": "https://discourse.roots.io/", + "issues": "https://github.com/roots/bedrock-disallow-indexing/issues", + "source": "https://github.com/roots/bedrock-disallow-indexing/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/roots", + "type": "github" + }, + { + "url": "https://www.patreon.com/rootsdev", + "type": "patreon" + } + ], + "time": "2020-05-20T01:25:07+00:00" + }, + { + "name": "roots/wordpress", + "version": "6.9.1", + "source": { + "type": "git", + "url": "https://github.com/roots/wordpress.git", + "reference": "29e4eb49b2f4c591e39d4eb6705a27cf1ea40e45" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roots/wordpress/zipball/29e4eb49b2f4c591e39d4eb6705a27cf1ea40e45", + "reference": "29e4eb49b2f4c591e39d4eb6705a27cf1ea40e45", + "shasum": "" + }, + "require": { + "roots/wordpress-core-installer": "^3.0", + "roots/wordpress-no-content": "self.version" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT", + "GPL-2.0-or-later" + ], + "description": "WordPress is open source software you can use to create a beautiful website, blog, or app.", + "homepage": "https://wordpress.org/", + "keywords": [ + "blog", + "cms", + "wordpress" + ], + "support": { + "issues": "https://github.com/roots/wordpress/issues", + "source": "https://github.com/roots/wordpress/tree/6.9.1" + }, + "funding": [ + { + "url": "https://github.com/roots", + "type": "github" + } + ], + "time": "2025-05-23T18:54:22+00:00" + }, + { + "name": "roots/wordpress-core-installer", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/roots/wordpress-core-installer.git", + "reference": "714d2e2a9e523f6e7bde4810d5a04aedf0ec217f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roots/wordpress-core-installer/zipball/714d2e2a9e523f6e7bde4810d5a04aedf0ec217f", + "reference": "714d2e2a9e523f6e7bde4810d5a04aedf0ec217f", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=7.2.24" + }, + "conflict": { + "composer/installers": "<1.0.6" + }, + "replace": { + "johnpbloch/wordpress-core-installer": "*" + }, + "require-dev": { + "composer/composer": "^1.0 || ^2.0", + "phpunit/phpunit": "^8.5" + }, + "type": "composer-plugin", + "extra": { + "class": "Roots\\Composer\\WordPressCorePlugin" + }, + "autoload": { + "psr-4": { + "Roots\\Composer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "John P. Bloch", + "email": "me@johnpbloch.com" + }, + { + "name": "Roots", + "email": "team@roots.io" + } + ], + "description": "A Composer custom installer to handle installing WordPress as a dependency", + "keywords": [ + "wordpress" + ], + "support": { + "issues": "https://github.com/roots/wordpress-core-installer/issues", + "source": "https://github.com/roots/wordpress-core-installer/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/roots", + "type": "github" + } + ], + "time": "2025-05-23T18:47:25+00:00" + }, + { + "name": "roots/wordpress-no-content", + "version": "6.9.1", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress.git", + "reference": "6.9.1" + }, + "dist": { + "type": "zip", + "url": "https://downloads.wordpress.org/release/wordpress-6.9.1-no-content.zip", + "reference": "6.9.1", + "shasum": "27121719e788b7c9e35cfcf8984ad8b42bd423d6" + }, + "require": { + "php": ">= 7.2.24" + }, + "provide": { + "wordpress/core-implementation": "6.9.1" + }, + "suggest": { + "ext-curl": "Performs remote request operations.", + "ext-dom": "Used to validate Text Widget content and to automatically configuring IIS7+.", + "ext-exif": "Works with metadata stored in images.", + "ext-fileinfo": "Used to detect mimetype of file uploads.", + "ext-hash": "Used for hashing, including passwords and update packages.", + "ext-imagick": "Provides better image quality for media uploads.", + "ext-json": "Used for communications with other servers.", + "ext-libsodium": "Validates Signatures and provides securely random bytes.", + "ext-mbstring": "Used to properly handle UTF8 text.", + "ext-mysqli": "Connects to MySQL for database interactions.", + "ext-openssl": "Permits SSL-based connections to other hosts.", + "ext-pcre": "Increases performance of pattern matching in code searches.", + "ext-xml": "Used for XML parsing, such as from a third-party site.", + "ext-zip": "Used for decompressing Plugins, Themes, and WordPress update packages." + }, + "type": "wordpress-core", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "WordPress Community", + "homepage": "https://wordpress.org/about/" + } + ], + "description": "WordPress is open source software you can use to create a beautiful website, blog, or app.", + "homepage": "https://wordpress.org/", + "keywords": [ + "blog", + "cms", + "wordpress" + ], + "support": { + "docs": "https://developer.wordpress.org/", + "forum": "https://wordpress.org/support/", + "irc": "irc://irc.freenode.net/wordpress", + "issues": "https://core.trac.wordpress.org/", + "rss": "https://wordpress.org/news/feed/", + "source": "https://core.trac.wordpress.org/browser", + "wiki": "https://codex.wordpress.org/" + }, + "funding": [ + { + "url": "https://wordpressfoundation.org/donate/", + "type": "other" + } + ], + "time": "2026-02-03T17:39:17+00:00" + }, + { + "name": "roots/wp-config", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/roots/wp-config.git", + "reference": "37c38230796119fb487fa03346ab0706ce6d4962" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roots/wp-config/zipball/37c38230796119fb487fa03346ab0706ce6d4962", + "reference": "37c38230796119fb487fa03346ab0706ce6d4962", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5.7", + "roave/security-advisories": "dev-master", + "squizlabs/php_codesniffer": "^3.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Roots\\WPConfig\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Austin Pray", + "email": "austin@austinpray.com" + } + ], + "description": "Collect configuration values and safely define() them", + "support": { + "issues": "https://github.com/roots/wp-config/issues", + "source": "https://github.com/roots/wp-config/tree/master" + }, + "time": "2018-08-10T14:18:38+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + } + ], + "packages-dev": [ + { + "name": "laravel/pint", + "version": "v1.27.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5", + "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.93.1", + "illuminate/view": "^12.51.0", + "larastan/larastan": "^3.9.2", + "laravel-zero/framework": "^12.0.5", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest": "^3.8.5" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-02-10T20:00:20+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.2" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/config/application.php b/config/application.php new file mode 100644 index 0000000..2dbb3a5 --- /dev/null +++ b/config/application.php @@ -0,0 +1,184 @@ +addAdapter(Dotenv\Repository\Adapter\EnvConstAdapter::class) + ->addAdapter(Dotenv\Repository\Adapter\PutenvAdapter::class) + ->immutable() + ->make(); + + $dotenv = Dotenv\Dotenv::create($repository, $root_dir, $env_files, false); + $dotenv->load(); + + $dotenv->required(['WP_HOME', 'WP_SITEURL']); + if (!env('DATABASE_URL')) { + $dotenv->required(['DB_NAME', 'DB_USER', 'DB_PASSWORD']); + } +} + +/** + * Set up our global environment constant. + * + * Default: production + */ +define('WP_ENV', env('WP_ENV') ?: 'production'); + +/** + * Infer WP_ENVIRONMENT_TYPE based on WP_ENV. + */ +if (!env('WP_ENVIRONMENT_TYPE') && in_array(WP_ENV, ['production', 'staging', 'development', 'local'], true)) { + Config::define('WP_ENVIRONMENT_TYPE', WP_ENV); +} + +/** + * URLs + */ +Config::define('WP_HOME', env('WP_HOME')); +Config::define('WP_SITEURL', env('WP_SITEURL')); + +/** + * Custom content directory + */ +Config::define('CONTENT_DIR', '/app'); +Config::define('WP_CONTENT_DIR', $webroot_dir . Config::get('CONTENT_DIR')); +Config::define('WP_CONTENT_URL', Config::get('WP_HOME') . Config::get('CONTENT_DIR')); + +/** + * Database + */ +if (env('DB_SSL')) { + Config::define('MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_SSL); +} + +Config::define('DB_NAME', env('DB_NAME')); +Config::define('DB_USER', env('DB_USER')); +Config::define('DB_PASSWORD', env('DB_PASSWORD')); +Config::define('DB_HOST', env('DB_HOST') ?: 'localhost'); +Config::define('DB_CHARSET', 'utf8mb4'); +Config::define('DB_COLLATE', ''); + +$table_prefix = env('DB_PREFIX') ?: 'wp_'; + +if (env('DATABASE_URL')) { + $dsn = (object) parse_url(env('DATABASE_URL')); + + Config::define('DB_NAME', substr($dsn->path, 1)); + Config::define('DB_USER', $dsn->user); + Config::define('DB_PASSWORD', isset($dsn->pass) ? $dsn->pass : null); + Config::define('DB_HOST', isset($dsn->port) ? "{$dsn->host}:{$dsn->port}" : $dsn->host); +} + +/** + * Authentication Unique Keys and Salts + */ +Config::define('AUTH_KEY', env('AUTH_KEY')); +Config::define('SECURE_AUTH_KEY', env('SECURE_AUTH_KEY')); +Config::define('LOGGED_IN_KEY', env('LOGGED_IN_KEY')); +Config::define('NONCE_KEY', env('NONCE_KEY')); +Config::define('AUTH_SALT', env('AUTH_SALT')); +Config::define('SECURE_AUTH_SALT', env('SECURE_AUTH_SALT')); +Config::define('LOGGED_IN_SALT', env('LOGGED_IN_SALT')); +Config::define('NONCE_SALT', env('NONCE_SALT')); + +/** + * Cloud-native / optional integrations + */ + +// Object cache (Redis) - plugin required (e.g. redis-cache). +Config::define('WP_CACHE', env('WP_CACHE') ?? false); +Config::define('WP_REDIS_HOST', env('WP_REDIS_HOST') ?: null); +Config::define('WP_REDIS_PORT', env('WP_REDIS_PORT') ?: 6379); +Config::define('WP_REDIS_PASSWORD', env('WP_REDIS_PASSWORD') ?: null); +Config::define('WP_REDIS_DATABASE', env('WP_REDIS_DATABASE') ?: 0); +Config::define('WP_REDIS_PREFIX', env('WP_REDIS_PREFIX') ?: $table_prefix); + +/** + * Custom settings / hardening + */ +Config::define('AUTOMATIC_UPDATER_DISABLED', true); +Config::define('DISABLE_WP_CRON', env('DISABLE_WP_CRON') ?: false); + +// Default theme shipped in this repository (can be overridden by env var). +Config::define('WP_DEFAULT_THEME', env('WP_DEFAULT_THEME') ?: 'starter-theme'); + +// Disable the plugin and theme file editor in the admin. +Config::define('DISALLOW_FILE_EDIT', true); + +// Disable plugin and theme updates and installation from the admin by default. +// Override in development/staging config if needed. +Config::define('DISALLOW_FILE_MODS', env('DISALLOW_FILE_MODS') ?? true); + +// Limit the number of post revisions. +Config::define('WP_POST_REVISIONS', env('WP_POST_REVISIONS') ?? true); + +// Disable script concatenation (avoid surprises behind CDNs/proxies). +Config::define('CONCATENATE_SCRIPTS', false); + +/** + * Debugging defaults (production-minded). + */ +Config::define('WP_DEBUG_DISPLAY', false); +Config::define('WP_DEBUG_LOG', false); +Config::define('SCRIPT_DEBUG', false); +ini_set('display_errors', '0'); + +/** + * Allow WordPress to detect HTTPS when used behind a reverse proxy/load balancer. + * See: https://codex.wordpress.org/Function_Reference/is_ssl#Notes + */ +if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { + $_SERVER['HTTPS'] = 'on'; +} + +$env_config = __DIR__ . '/environments/' . WP_ENV . '.php'; +if (file_exists($env_config)) { + require_once $env_config; +} + +Config::apply(); + +/** + * Bootstrap WordPress + */ +if (!defined('ABSPATH')) { + define('ABSPATH', $webroot_dir . '/wp/'); +} + diff --git a/config/environments/development.php b/config/environments/development.php new file mode 100644 index 0000000..27ac8c5 --- /dev/null +++ b/config/environments/development.php @@ -0,0 +1,23 @@ + +
+
+

Starter theme placeholder.

+
+ + + + + diff --git a/web/app/themes/starter-theme/functions.php b/web/app/themes/starter-theme/functions.php new file mode 100644 index 0000000..fa5d258 --- /dev/null +++ b/web/app/themes/starter-theme/functions.php @@ -0,0 +1,13 @@ + +> + + + + + +> + +
+
+

+

+
+ diff --git a/web/app/themes/starter-theme/index.php b/web/app/themes/starter-theme/index.php new file mode 100644 index 0000000..54177bf --- /dev/null +++ b/web/app/themes/starter-theme/index.php @@ -0,0 +1,25 @@ + + +
+ + +
> +

+
+ +
+
+ + +

No posts found.

+ +
+ + Date: Wed, 11 Feb 2026 07:47:49 +0000 Subject: [PATCH 03/29] feat: add Docker Compose and hardened container builds Co-authored-by: Manuel H. --- .dockerignore | 18 +++ Dockerfile | 128 +++++++++++++++ Makefile | 42 +++++ compose.prod.yaml | 62 ++++++++ compose.yaml | 164 ++++++++++++++++++++ docker/nginx/conf.d/default.conf | 84 ++++++++++ docker/nginx/snippets/security-headers.conf | 6 + docker/php/conf.d/50-apcu.ini | 5 + docker/php/conf.d/99-opcache-dev.ini | 14 ++ docker/php/conf.d/99-opcache-prod.ini | 14 ++ docker/php/fpm-pool.conf | 23 +++ docker/php/php.ini | 18 +++ 12 files changed, 578 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 compose.prod.yaml create mode 100644 compose.yaml create mode 100644 docker/nginx/conf.d/default.conf create mode 100644 docker/nginx/snippets/security-headers.conf create mode 100644 docker/php/conf.d/50-apcu.ini create mode 100644 docker/php/conf.d/99-opcache-dev.ini create mode 100644 docker/php/conf.d/99-opcache-prod.ini create mode 100644 docker/php/fpm-pool.conf create mode 100644 docker/php/php.ini diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..264c9db --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git +.github +helm + +.env +.env.* + +node_modules + +# Local/IDE noise +.idea +.vscode +.DS_Store + +# Composer install output (built inside Docker) +vendor +web/wp + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6cebb1d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,128 @@ +# syntax=docker/dockerfile:1.7 + +ARG PHP_VERSION=8.3 +ARG NGINX_VERSION=1.27 + +FROM composer:2 AS composer + +# ----------------------------------------------------------------------------- +# PHP base (extensions + runtime libs) +# ----------------------------------------------------------------------------- +FROM php:${PHP_VERSION}-fpm-alpine AS php-base + +RUN set -eux; \ + apk add --no-cache \ + icu-libs \ + libzip \ + libpng \ + libjpeg-turbo \ + freetype \ + ; \ + apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + icu-dev \ + libzip-dev \ + libpng-dev \ + libjpeg-turbo-dev \ + freetype-dev \ + ; \ + docker-php-ext-configure gd --with-freetype --with-jpeg; \ + docker-php-ext-install -j"$(nproc)" \ + intl \ + mysqli \ + opcache \ + pdo_mysql \ + zip \ + gd \ + ; \ + pecl install redis apcu; \ + docker-php-ext-enable redis apcu opcache; \ + apk del .build-deps + +# ----------------------------------------------------------------------------- +# Build stage: composer install (no-dev) to produce vendor/ + web/wp +# ----------------------------------------------------------------------------- +FROM php-base AS build + +WORKDIR /var/www/html + +COPY --from=composer /usr/bin/composer /usr/local/bin/composer + +ENV COMPOSER_ALLOW_SUPERUSER=1 \ + COMPOSER_HOME=/tmp/composer + +COPY composer.json composer.lock ./ + +RUN --mount=type=cache,target=/tmp/composer/cache \ + composer install \ + --no-dev \ + --no-interaction \ + --no-progress \ + --prefer-dist + +COPY config/ config/ +COPY web/ web/ + +# ----------------------------------------------------------------------------- +# PHP runtime (non-root, production-minded) +# ----------------------------------------------------------------------------- +FROM php-base AS php-runtime + +WORKDIR /var/www/html + +RUN addgroup -g 10001 -S app && adduser -u 10001 -S -G app app + +# Replace the default pool with our socket-based pool. +RUN rm -f /usr/local/etc/php-fpm.d/www.conf +COPY docker/php/fpm-pool.conf /usr/local/etc/php-fpm.d/zz-app.conf + +COPY docker/php/php.ini /usr/local/etc/php/php.ini +COPY docker/php/conf.d/50-apcu.ini /usr/local/etc/php/conf.d/50-apcu.ini +COPY docker/php/conf.d/99-opcache-prod.ini /usr/local/etc/php/conf.d/99-opcache.ini + +COPY --from=build --chown=app:app /var/www/html /var/www/html + +# Writable paths are expected to be mounted in production: +# - /tmp (tmpfs) +# - /var/run/php (socket) +# - web/app/uploads (uploads) +# - web/app/cache (caches) +RUN mkdir -p /tmp /var/run/php /var/www/html/web/app/uploads /var/www/html/web/app/cache && \ + chown -R app:app /var/run/php /var/www/html/web/app/uploads /var/www/html/web/app/cache + +USER app + +EXPOSE 9000 + +CMD ["php-fpm", "-F"] + +# ----------------------------------------------------------------------------- +# PHP dev (includes Composer + dev-friendly OPcache) +# ----------------------------------------------------------------------------- +FROM php-runtime AS php-dev + +USER root +COPY --from=composer /usr/bin/composer /usr/local/bin/composer +COPY docker/php/conf.d/99-opcache-dev.ini /usr/local/etc/php/conf.d/99-opcache.ini +USER app + +# ----------------------------------------------------------------------------- +# Nginx runtime (non-root) +# ----------------------------------------------------------------------------- +FROM nginxinc/nginx-unprivileged:${NGINX_VERSION}-alpine AS nginx-runtime + +WORKDIR /var/www/html + +USER root +COPY docker/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf +COPY docker/nginx/snippets/ /etc/nginx/snippets/ +COPY --from=build --chown=101:101 /var/www/html/web /var/www/html/web +USER 101 + +EXPOSE 8080 + +# ----------------------------------------------------------------------------- +# Nginx dev (same image; source is bind-mounted in Compose) +# ----------------------------------------------------------------------------- +FROM nginx-runtime AS nginx-dev + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3591ba8 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +SHELL := /usr/bin/env bash + +COMPOSE ?= docker compose + +.PHONY: help up down restart ps logs shell up-mail up-dbadmin up-observability wp composer + +help: ## Show available targets + @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_.-]+:.*##/ {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +up: ## Start dev stack (build + up) + $(COMPOSE) up -d --build + +down: ## Stop dev stack + $(COMPOSE) down --remove-orphans + +restart: ## Restart dev stack + $(COMPOSE) restart + +ps: ## Show container status + $(COMPOSE) ps + +logs: ## Tail logs (set SERVICE=... to filter) + @if [ -n "$(SERVICE)" ]; then $(COMPOSE) logs -f --tail=200 "$(SERVICE)"; else $(COMPOSE) logs -f --tail=200; fi + +shell: ## Shell into PHP container + $(COMPOSE) exec php sh + +up-mail: ## Start stack with MailHog profile + $(COMPOSE) --profile mail up -d --build + +up-dbadmin: ## Start stack with phpMyAdmin profile + $(COMPOSE) --profile dbadmin up -d --build + +up-observability: ## Start stack with exporters profile + $(COMPOSE) --profile observability up -d --build + +wp: ## Run WP-CLI (example: make wp ARGS="core version") + $(COMPOSE) --profile tools run --rm wp $(ARGS) + +composer: ## Run Composer in a container (example: make composer ARGS="install") + $(COMPOSE) --profile tools run --rm composer $(ARGS) + diff --git a/compose.prod.yaml b/compose.prod.yaml new file mode 100644 index 0000000..b9a2948 --- /dev/null +++ b/compose.prod.yaml @@ -0,0 +1,62 @@ +services: + web: + image: ${IMAGE_WEB:-ghcr.io/OWNER/REPO-nginx:main} + ports: + - "8080:8080" + depends_on: + - php + read_only: true + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + tmpfs: + - /tmp:rw,noexec,nosuid,size=64m + volumes: + - php-socket:/var/run/php + - uploads:/var/www/html/web/app/uploads + - cache:/var/www/html/web/app/cache + + php: + image: ${IMAGE_PHP:-ghcr.io/OWNER/REPO-php:main} + env_file: + - .env + depends_on: + - db + - redis + read_only: true + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + tmpfs: + - /tmp:rw,noexec,nosuid,size=128m + - /var/run/php:rw,nosuid,size=8m + volumes: + - php-socket:/var/run/php + - uploads:/var/www/html/web/app/uploads + - cache:/var/www/html/web/app/cache + + db: + image: mariadb:11.4 + environment: + MARIADB_DATABASE: ${DB_NAME:-wordpress} + MARIADB_USER: ${DB_USER:-wordpress} + MARIADB_PASSWORD: ${DB_PASSWORD:-wordpress} + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root} + volumes: + - db-data:/var/lib/mysql + + redis: + image: redis:7-alpine + command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] + volumes: + - redis-data:/data + +volumes: + db-data: + redis-data: + php-socket: + uploads: + cache: + diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..dfddad7 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,164 @@ +services: + # --------------------------------------------------------------------------- + # Web server (Nginx) - serves static files and forwards PHP to php-fpm socket. + # --------------------------------------------------------------------------- + web: + build: + context: . + dockerfile: Dockerfile + target: nginx-dev + ports: + - "8080:8080" + depends_on: + - php + volumes: + - ./:/var/www/html:ro + - php-socket:/var/run/php + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz >/dev/null 2>&1 || exit 1"] + interval: 10s + timeout: 3s + retries: 10 + + # --------------------------------------------------------------------------- + # PHP-FPM - Bedrock + WordPress via Composer. + # --------------------------------------------------------------------------- + php: + build: + context: . + dockerfile: Dockerfile + target: php-dev + env_file: + - .env + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ./:/var/www/html + - php-socket:/var/run/php + + # --------------------------------------------------------------------------- + # Database + # --------------------------------------------------------------------------- + db: + image: mariadb:11.4 + environment: + MARIADB_DATABASE: ${DB_NAME:-wordpress} + MARIADB_USER: ${DB_USER:-wordpress} + MARIADB_PASSWORD: ${DB_PASSWORD:-wordpress} + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root} + volumes: + - db-data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -u root -p$$MARIADB_ROOT_PASSWORD --silent"] + interval: 10s + timeout: 5s + retries: 10 + + # --------------------------------------------------------------------------- + # Redis (optional object cache) + # --------------------------------------------------------------------------- + redis: + image: redis:7-alpine + command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 10 + + # --------------------------------------------------------------------------- + # Optional: MailHog (captures outgoing email for local dev) + # UI: http://localhost:8025 + # --------------------------------------------------------------------------- + mailhog: + image: mailhog/mailhog:v1.0.1 + profiles: ["mail"] + ports: + - "8025:8025" + - "1025:1025" + + # --------------------------------------------------------------------------- + # Optional: phpMyAdmin (dev-only DB admin) + # UI: http://localhost:8081 + # --------------------------------------------------------------------------- + phpmyadmin: + image: phpmyadmin:5 + profiles: ["dbadmin"] + environment: + PMA_HOST: db + PMA_USER: ${DB_USER:-wordpress} + PMA_PASSWORD: ${DB_PASSWORD:-wordpress} + ports: + - "8081:80" + depends_on: + db: + condition: service_healthy + + # --------------------------------------------------------------------------- + # Optional tools (run on-demand via `make wp ...` / `make composer ...`) + # --------------------------------------------------------------------------- + wp: + image: wordpress:cli-php8.3 + profiles: ["tools"] + env_file: + - .env + working_dir: /var/www/html + volumes: + - ./:/var/www/html + depends_on: + db: + condition: service_healthy + + composer: + image: composer:2 + profiles: ["tools"] + working_dir: /var/www/html + volumes: + - ./:/var/www/html + + # --------------------------------------------------------------------------- + # Optional observability: exporters (Prometheus scrape targets) + # --------------------------------------------------------------------------- + redis-exporter: + image: oliver006/redis_exporter:latest + profiles: ["observability"] + environment: + REDIS_ADDR: redis://redis:6379 + ports: + - "9121:9121" + depends_on: + redis: + condition: service_healthy + + mysqld-exporter: + image: prom/mysqld-exporter:latest + profiles: ["observability"] + environment: + DATA_SOURCE_NAME: root:${DB_ROOT_PASSWORD:-root}@(db:3306)/ + ports: + - "9104:9104" + depends_on: + db: + condition: service_healthy + + nginx-exporter: + image: nginx/nginx-prometheus-exporter:latest + profiles: ["observability"] + command: + - "--nginx.scrape-uri=http://web:8080/nginx_status" + ports: + - "9113:9113" + depends_on: + web: + condition: service_healthy + +volumes: + db-data: + redis-data: + php-socket: + diff --git a/docker/nginx/conf.d/default.conf b/docker/nginx/conf.d/default.conf new file mode 100644 index 0000000..9e08fa2 --- /dev/null +++ b/docker/nginx/conf.d/default.conf @@ -0,0 +1,84 @@ +server { + listen 8080 default_server; + server_name _; + + root /var/www/html/web; + index index.php; + + include /etc/nginx/snippets/security-headers.conf; + + # Basic liveness probe + location = /healthz { + access_log off; + add_header Content-Type text/plain; + return 200 "ok\n"; + } + + # Nginx stub_status for Prometheus exporter (restricted to private ranges). + location = /nginx_status { + stub_status; + access_log off; + allow 127.0.0.1; + allow 10.0.0.0/8; + allow 172.16.0.0/12; + allow 192.168.0.0/16; + deny all; + } + + # Don't serve hidden files (.env, .git, etc). + location ~ /\.(?!well-known).* { + deny all; + } + + # Block common sensitive files (defense-in-depth). + location ~* /(composer\.(json|lock)|\.env|\.git|wp-config\.php)$ { + deny all; + } + + # Cache static assets. + location ~* \.(?:css|js|jpg|jpeg|gif|png|webp|svg|ico|woff2?|ttf|eot)$ { + expires 7d; + access_log off; + add_header Cache-Control "public"; + try_files $uri =404; + } + + # WordPress / Bedrock front controller. + location / { + try_files $uri $uri/ /index.php?$args; + } + + # PHP handler. + location ~ \.php$ { + try_files $uri =404; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_index index.php; + fastcgi_read_timeout 60s; + fastcgi_pass unix:/var/run/php/php-fpm.sock; + } + + # php-fpm ping/status endpoints + location = /ping { + access_log off; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + fastcgi_param SCRIPT_NAME /index.php; + fastcgi_pass unix:/var/run/php/php-fpm.sock; + } + + location = /status { + access_log off; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + fastcgi_param SCRIPT_NAME /index.php; + fastcgi_pass unix:/var/run/php/php-fpm.sock; + allow 127.0.0.1; + allow 10.0.0.0/8; + allow 172.16.0.0/12; + allow 192.168.0.0/16; + deny all; + } +} + diff --git a/docker/nginx/snippets/security-headers.conf b/docker/nginx/snippets/security-headers.conf new file mode 100644 index 0000000..bb05b46 --- /dev/null +++ b/docker/nginx/snippets/security-headers.conf @@ -0,0 +1,6 @@ +# Security headers (safe defaults; adjust per your needs) +add_header X-Content-Type-Options "nosniff" always; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +add_header Permissions-Policy "interest-cohort=()" always; + diff --git a/docker/php/conf.d/50-apcu.ini b/docker/php/conf.d/50-apcu.ini new file mode 100644 index 0000000..3be82d1 --- /dev/null +++ b/docker/php/conf.d/50-apcu.ini @@ -0,0 +1,5 @@ +; APCu (optional application cache) +apc.enabled=1 +apc.enable_cli=0 +apc.shm_size=128M + diff --git a/docker/php/conf.d/99-opcache-dev.ini b/docker/php/conf.d/99-opcache-dev.ini new file mode 100644 index 0000000..345e87e --- /dev/null +++ b/docker/php/conf.d/99-opcache-dev.ini @@ -0,0 +1,14 @@ +; OPcache (development-friendly) +opcache.enable=1 +opcache.enable_cli=0 + +; Revalidate on file changes for bind-mounted source. +opcache.validate_timestamps=1 +opcache.revalidate_freq=1 + +opcache.memory_consumption=192 +opcache.interned_strings_buffer=16 +opcache.max_accelerated_files=20000 +opcache.max_wasted_percentage=10 +opcache.jit=0 + diff --git a/docker/php/conf.d/99-opcache-prod.ini b/docker/php/conf.d/99-opcache-prod.ini new file mode 100644 index 0000000..6f418a2 --- /dev/null +++ b/docker/php/conf.d/99-opcache-prod.ini @@ -0,0 +1,14 @@ +; OPcache (production-minded) +opcache.enable=1 +opcache.enable_cli=0 + +; Revalidate is intentionally disabled for immutable container images. +opcache.validate_timestamps=0 +opcache.revalidate_freq=0 + +opcache.memory_consumption=192 +opcache.interned_strings_buffer=16 +opcache.max_accelerated_files=20000 +opcache.max_wasted_percentage=10 +opcache.jit=0 + diff --git a/docker/php/fpm-pool.conf b/docker/php/fpm-pool.conf new file mode 100644 index 0000000..5e03d3c --- /dev/null +++ b/docker/php/fpm-pool.conf @@ -0,0 +1,23 @@ +[www] +user = app +group = app + +listen = /var/run/php/php-fpm.sock +listen.owner = app +listen.group = app +listen.mode = 0660 + +pm = dynamic +pm.max_children = 20 +pm.start_servers = 2 +pm.min_spare_servers = 2 +pm.max_spare_servers = 6 +pm.max_requests = 500 + +clear_env = no +catch_workers_output = yes + +; Health endpoints (proxied by Nginx) +ping.path = /ping +pm.status_path = /status + diff --git a/docker/php/php.ini b/docker/php/php.ini new file mode 100644 index 0000000..339fae1 --- /dev/null +++ b/docker/php/php.ini @@ -0,0 +1,18 @@ +; Minimal baseline php.ini (container-friendly) + +expose_php = 0 + +memory_limit = 256M +max_execution_time = 60 +max_input_vars = 2000 + +post_max_size = 64M +upload_max_filesize = 64M + +; Send errors to stderr for container logs. +log_errors = On +error_log = /proc/self/fd/2 +display_errors = Off + +date.timezone = UTC + From 9d270284c2b6b684a2cfc097016f7daaceedaf62 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 07:55:53 +0000 Subject: [PATCH 04/29] feat: add Helm chart for Kubernetes deployment Co-authored-by: Manuel H. --- helm/wp-boilerplate/.helmignore | 5 + helm/wp-boilerplate/Chart.yaml | 7 + helm/wp-boilerplate/README.md | 77 ++++++++++ helm/wp-boilerplate/templates/_helpers.tpl | 48 ++++++ helm/wp-boilerplate/templates/configmap.yaml | 26 ++++ helm/wp-boilerplate/templates/cronjob.yaml | 42 ++++++ helm/wp-boilerplate/templates/deployment.yaml | 110 ++++++++++++++ helm/wp-boilerplate/templates/hpa.yaml | 23 +++ helm/wp-boilerplate/templates/ingress.yaml | 34 +++++ .../templates/networkpolicy.yaml | 22 +++ helm/wp-boilerplate/templates/pdb.yaml | 14 ++ .../wp-boilerplate/templates/pvc-uploads.yaml | 18 +++ helm/wp-boilerplate/templates/secret.yaml | 14 ++ helm/wp-boilerplate/templates/service.yaml | 15 ++ .../templates/serviceaccount.yaml | 9 ++ helm/wp-boilerplate/values.yaml | 140 ++++++++++++++++++ 16 files changed, 604 insertions(+) create mode 100644 helm/wp-boilerplate/.helmignore create mode 100644 helm/wp-boilerplate/Chart.yaml create mode 100644 helm/wp-boilerplate/README.md create mode 100644 helm/wp-boilerplate/templates/_helpers.tpl create mode 100644 helm/wp-boilerplate/templates/configmap.yaml create mode 100644 helm/wp-boilerplate/templates/cronjob.yaml create mode 100644 helm/wp-boilerplate/templates/deployment.yaml create mode 100644 helm/wp-boilerplate/templates/hpa.yaml create mode 100644 helm/wp-boilerplate/templates/ingress.yaml create mode 100644 helm/wp-boilerplate/templates/networkpolicy.yaml create mode 100644 helm/wp-boilerplate/templates/pdb.yaml create mode 100644 helm/wp-boilerplate/templates/pvc-uploads.yaml create mode 100644 helm/wp-boilerplate/templates/secret.yaml create mode 100644 helm/wp-boilerplate/templates/service.yaml create mode 100644 helm/wp-boilerplate/templates/serviceaccount.yaml create mode 100644 helm/wp-boilerplate/values.yaml diff --git a/helm/wp-boilerplate/.helmignore b/helm/wp-boilerplate/.helmignore new file mode 100644 index 0000000..4766564 --- /dev/null +++ b/helm/wp-boilerplate/.helmignore @@ -0,0 +1,5 @@ +.DS_Store +.git/ +.idea/ +.vscode/ + diff --git a/helm/wp-boilerplate/Chart.yaml b/helm/wp-boilerplate/Chart.yaml new file mode 100644 index 0000000..2977dc1 --- /dev/null +++ b/helm/wp-boilerplate/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: wp-boilerplate +description: Cloud-native WordPress (Bedrock) boilerplate +type: application +version: 0.1.0 +appVersion: "0.1.0" + diff --git a/helm/wp-boilerplate/README.md b/helm/wp-boilerplate/README.md new file mode 100644 index 0000000..5b02b13 --- /dev/null +++ b/helm/wp-boilerplate/README.md @@ -0,0 +1,77 @@ +# wp-boilerplate Helm Chart + +This chart deploys the boilerplate as a hardened, Kubernetes-native workload: + +- **Nginx** (non-root) serving Bedrock `web/` +- **PHP-FPM** (non-root) running WordPress +- Shared **FPM socket** via `emptyDir` +- Optional **uploads PVC** (or offload uploads to object storage) +- Optional **HPA / PDB / NetworkPolicy** +- Optional **CronJob** to replace WP-Cron + +## Quick install + +> Recommended: deploy **by image digest** (build once, deploy everywhere). + +```bash +helm upgrade --install wp ./helm/wp-boilerplate \ + --namespace wp --create-namespace \ + --set image.php.repository=ghcr.io/OWNER/REPO-php \ + --set image.web.repository=ghcr.io/OWNER/REPO-nginx \ + --set image.php.digest=sha256:... \ + --set image.web.digest=sha256:... +``` + +## Configuration (ConfigMap + Secret) + +The app uses Bedrock-style env vars. + +This chart creates a ConfigMap (`-env`) unless `config.existingConfigMap` is provided. + +### Secrets + +For production, prefer **External Secrets** or **SealedSecrets**. + +You can either: + +- Provide `secret.existingSecret`, or +- Set `secret.create=true` and populate `secret.data` (not recommended) + +Expected Secret keys include: + +- `DB_PASSWORD` +- `AUTH_KEY`, `SECURE_AUTH_KEY`, `LOGGED_IN_KEY`, `NONCE_KEY` +- `AUTH_SALT`, `SECURE_AUTH_SALT`, `LOGGED_IN_SALT`, `NONCE_SALT` + +## Uploads storage strategy + +### Recommended (cloud-native): S3-compatible offload + +Use a uploads offload plugin (example: `humanmade/s3-uploads`) so you can scale replicas without shared storage. + +### Alternative: PVC + +Set: + +```yaml +uploads: + persistence: + enabled: true + size: 10Gi +``` + +If you scale above 1 replica without a shared RWX filesystem, uploads consistency will break. + +## WP-Cron + +Production guidance: + +- Set `DISABLE_WP_CRON=true` +- Enable the chart CronJob: + +```yaml +cron: + enabled: true + schedule: "*/5 * * * *" +``` + diff --git a/helm/wp-boilerplate/templates/_helpers.tpl b/helm/wp-boilerplate/templates/_helpers.tpl new file mode 100644 index 0000000..8533e1a --- /dev/null +++ b/helm/wp-boilerplate/templates/_helpers.tpl @@ -0,0 +1,48 @@ +{{- define "wp-boilerplate.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "wp-boilerplate.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := include "wp-boilerplate.name" . -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "wp-boilerplate.labels" -}} +app.kubernetes.io/name: {{ include "wp-boilerplate.name" . }} +helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{- define "wp-boilerplate.selectorLabels" -}} +app.kubernetes.io/name: {{ include "wp-boilerplate.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{- define "wp-boilerplate.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "wp-boilerplate.fullname" .) .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end -}} + +{{- define "wp-boilerplate.imageRef" -}} +{{- $img := . -}} +{{- if $img.digest -}} +{{- printf "%s@%s" $img.repository $img.digest -}} +{{- else if $img.tag -}} +{{- printf "%s:%s" $img.repository $img.tag -}} +{{- else -}} +{{- fail "image.tag or image.digest must be set (prefer digest for build-once deploy-everywhere)" -}} +{{- end -}} +{{- end -}} + diff --git a/helm/wp-boilerplate/templates/configmap.yaml b/helm/wp-boilerplate/templates/configmap.yaml new file mode 100644 index 0000000..4d4ece0 --- /dev/null +++ b/helm/wp-boilerplate/templates/configmap.yaml @@ -0,0 +1,26 @@ +{{- if not .Values.config.existingConfigMap -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "wp-boilerplate.fullname" . }}-env + labels: + {{- include "wp-boilerplate.labels" . | nindent 4 }} +data: + WP_ENV: {{ .Values.env.WP_ENV | quote }} + WP_HOME: {{ .Values.env.WP_HOME | quote }} + WP_SITEURL: {{ .Values.env.WP_SITEURL | quote }} + + DISABLE_WP_CRON: {{ .Values.env.DISABLE_WP_CRON | quote }} + DISALLOW_FILE_MODS: {{ .Values.env.DISALLOW_FILE_MODS | quote }} + WP_CACHE: {{ .Values.env.WP_CACHE | quote }} + WP_DEFAULT_THEME: {{ .Values.env.WP_DEFAULT_THEME | quote }} + + WP_REDIS_HOST: {{ .Values.env.WP_REDIS_HOST | quote }} + WP_REDIS_PORT: {{ .Values.env.WP_REDIS_PORT | quote }} + + DB_HOST: {{ .Values.env.DB_HOST | quote }} + DB_NAME: {{ .Values.env.DB_NAME | quote }} + DB_USER: {{ .Values.env.DB_USER | quote }} + DB_PREFIX: {{ .Values.env.DB_PREFIX | quote }} +{{- end }} + diff --git a/helm/wp-boilerplate/templates/cronjob.yaml b/helm/wp-boilerplate/templates/cronjob.yaml new file mode 100644 index 0000000..9ece66e --- /dev/null +++ b/helm/wp-boilerplate/templates/cronjob.yaml @@ -0,0 +1,42 @@ +{{- if .Values.cron.enabled -}} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "wp-boilerplate.fullname" . }}-cron + labels: + {{- include "wp-boilerplate.labels" . | nindent 4 }} +spec: + schedule: {{ .Values.cron.schedule | quote }} + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 1 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + template: + metadata: + labels: + {{- include "wp-boilerplate.selectorLabels" . | nindent 12 }} + spec: + restartPolicy: OnFailure + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: wp-cron + image: {{ printf "%s:%s" .Values.cron.image.repository .Values.cron.image.tag }} + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: ["ALL"] + command: + - /bin/sh + - -c + - > + curl -fsSL + "http://{{ include "wp-boilerplate.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }}{{ .Values.cron.urlPath }}" + >/dev/null +{{- end }} + diff --git a/helm/wp-boilerplate/templates/deployment.yaml b/helm/wp-boilerplate/templates/deployment.yaml new file mode 100644 index 0000000..dceb36e --- /dev/null +++ b/helm/wp-boilerplate/templates/deployment.yaml @@ -0,0 +1,110 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "wp-boilerplate.fullname" . }} + labels: + {{- include "wp-boilerplate.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "wp-boilerplate.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "wp-boilerplate.selectorLabels" . | nindent 8 }} + annotations: + {{- toYaml .Values.podAnnotations | nindent 8 }} + spec: + serviceAccountName: {{ include "wp-boilerplate.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + volumes: + - name: php-socket + emptyDir: {} + - name: tmp + emptyDir: + medium: Memory + - name: nginx-cache + emptyDir: {} + - name: uploads +{{ if .Values.uploads.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ default (printf "%s-uploads" (include "wp-boilerplate.fullname" .)) .Values.uploads.persistence.existingClaim }} +{{ else }} + emptyDir: {} +{{ end }} + - name: app-cache + emptyDir: {} + + containers: + - name: web + image: {{ include "wp-boilerplate.imageRef" .Values.image.web }} + imagePullPolicy: {{ .Values.image.web.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.targetPort }} + securityContext: + {{- toYaml .Values.securityContext.web | nindent 12 }} + volumeMounts: + - name: php-socket + mountPath: /var/run/php + - name: tmp + mountPath: /tmp + - name: nginx-cache + mountPath: /var/cache/nginx + - name: uploads + mountPath: /var/www/html/web/app/uploads + - name: app-cache + mountPath: /var/www/html/web/app/cache + readinessProbe: + httpGet: + path: /ping + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + resources: + {{- toYaml .Values.resources.web | nindent 12 }} + + - name: php + image: {{ include "wp-boilerplate.imageRef" .Values.image.php }} + imagePullPolicy: {{ .Values.image.php.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext.php | nindent 12 }} + envFrom: + - configMapRef: + name: {{ default (printf "%s-env" (include "wp-boilerplate.fullname" .)) .Values.config.existingConfigMap }} + {{- if .Values.secret.existingSecret }} + - secretRef: + name: {{ .Values.secret.existingSecret }} + {{- else if .Values.secret.create }} + - secretRef: + name: {{ include "wp-boilerplate.fullname" . }}-secret + {{- end }} + env: + {{- range .Values.env.extra }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + volumeMounts: + - name: php-socket + mountPath: /var/run/php + - name: tmp + mountPath: /tmp + - name: uploads + mountPath: /var/www/html/web/app/uploads + - name: app-cache + mountPath: /var/www/html/web/app/cache + resources: + {{- toYaml .Values.resources.php | nindent 12 }} + diff --git a/helm/wp-boilerplate/templates/hpa.yaml b/helm/wp-boilerplate/templates/hpa.yaml new file mode 100644 index 0000000..2ab5e77 --- /dev/null +++ b/helm/wp-boilerplate/templates/hpa.yaml @@ -0,0 +1,23 @@ +{{- if .Values.autoscaling.enabled -}} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "wp-boilerplate.fullname" . }} + labels: + {{- include "wp-boilerplate.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "wp-boilerplate.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} +{{- end }} + diff --git a/helm/wp-boilerplate/templates/ingress.yaml b/helm/wp-boilerplate/templates/ingress.yaml new file mode 100644 index 0000000..a451478 --- /dev/null +++ b/helm/wp-boilerplate/templates/ingress.yaml @@ -0,0 +1,34 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "wp-boilerplate.fullname" . }} + labels: + {{- include "wp-boilerplate.labels" . | nindent 4 }} + annotations: + {{- toYaml .Values.ingress.annotations | nindent 4 }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- toYaml .Values.ingress.tls | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path | quote }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "wp-boilerplate.fullname" $ }} + port: + name: http + {{- end }} + {{- end }} +{{- end }} + diff --git a/helm/wp-boilerplate/templates/networkpolicy.yaml b/helm/wp-boilerplate/templates/networkpolicy.yaml new file mode 100644 index 0000000..70d9d04 --- /dev/null +++ b/helm/wp-boilerplate/templates/networkpolicy.yaml @@ -0,0 +1,22 @@ +{{- if .Values.networkPolicy.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "wp-boilerplate.fullname" . }} + labels: + {{- include "wp-boilerplate.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "wp-boilerplate.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + - Egress + ingress: + - ports: + - protocol: TCP + port: {{ .Values.service.targetPort }} + egress: + - {} +{{- end }} + diff --git a/helm/wp-boilerplate/templates/pdb.yaml b/helm/wp-boilerplate/templates/pdb.yaml new file mode 100644 index 0000000..50577da --- /dev/null +++ b/helm/wp-boilerplate/templates/pdb.yaml @@ -0,0 +1,14 @@ +{{- if .Values.pdb.enabled -}} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "wp-boilerplate.fullname" . }} + labels: + {{- include "wp-boilerplate.labels" . | nindent 4 }} +spec: + minAvailable: {{ .Values.pdb.minAvailable }} + selector: + matchLabels: + {{- include "wp-boilerplate.selectorLabels" . | nindent 6 }} +{{- end }} + diff --git a/helm/wp-boilerplate/templates/pvc-uploads.yaml b/helm/wp-boilerplate/templates/pvc-uploads.yaml new file mode 100644 index 0000000..7bdbf5b --- /dev/null +++ b/helm/wp-boilerplate/templates/pvc-uploads.yaml @@ -0,0 +1,18 @@ +{{- if and .Values.uploads.persistence.enabled (not .Values.uploads.persistence.existingClaim) -}} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "wp-boilerplate.fullname" . }}-uploads + labels: + {{- include "wp-boilerplate.labels" . | nindent 4 }} +spec: + accessModes: + {{- toYaml .Values.uploads.persistence.accessModes | nindent 4 }} + resources: + requests: + storage: {{ .Values.uploads.persistence.size | quote }} + {{- if .Values.uploads.persistence.storageClass }} + storageClassName: {{ .Values.uploads.persistence.storageClass | quote }} + {{- end }} +{{- end }} + diff --git a/helm/wp-boilerplate/templates/secret.yaml b/helm/wp-boilerplate/templates/secret.yaml new file mode 100644 index 0000000..5f2877e --- /dev/null +++ b/helm/wp-boilerplate/templates/secret.yaml @@ -0,0 +1,14 @@ +{{- if and (not .Values.secret.existingSecret) .Values.secret.create -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "wp-boilerplate.fullname" . }}-secret + labels: + {{- include "wp-boilerplate.labels" . | nindent 4 }} +type: Opaque +stringData: +{{- range $k, $v := .Values.secret.data }} + {{ $k }}: {{ $v | quote }} +{{- end }} +{{- end }} + diff --git a/helm/wp-boilerplate/templates/service.yaml b/helm/wp-boilerplate/templates/service.yaml new file mode 100644 index 0000000..1eff073 --- /dev/null +++ b/helm/wp-boilerplate/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "wp-boilerplate.fullname" . }} + labels: + {{- include "wp-boilerplate.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "wp-boilerplate.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + diff --git a/helm/wp-boilerplate/templates/serviceaccount.yaml b/helm/wp-boilerplate/templates/serviceaccount.yaml new file mode 100644 index 0000000..9a89e8b --- /dev/null +++ b/helm/wp-boilerplate/templates/serviceaccount.yaml @@ -0,0 +1,9 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "wp-boilerplate.serviceAccountName" . }} + labels: + {{- include "wp-boilerplate.labels" . | nindent 4 }} +{{- end }} + diff --git a/helm/wp-boilerplate/values.yaml b/helm/wp-boilerplate/values.yaml new file mode 100644 index 0000000..3f42936 --- /dev/null +++ b/helm/wp-boilerplate/values.yaml @@ -0,0 +1,140 @@ +replicaCount: 1 + +image: + php: + repository: ghcr.io/OWNER/REPO-php + tag: "" + digest: "" + pullPolicy: IfNotPresent + web: + repository: ghcr.io/OWNER/REPO-nginx + tag: "" + digest: "" + pullPolicy: IfNotPresent + +serviceAccount: + create: false + name: "" + +podAnnotations: {} + +podSecurityContext: + seccompProfile: + type: RuntimeDefault + +securityContext: + php: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 10001 + capabilities: + drop: ["ALL"] + web: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 101 + capabilities: + drop: ["ALL"] + +service: + type: ClusterIP + port: 80 + targetPort: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: wp.example.com + paths: + - path: / + pathType: Prefix + tls: [] + +resources: + php: {} + web: {} + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + +pdb: + enabled: false + minAvailable: 1 + +networkPolicy: + enabled: false + +env: + # Bedrock expects these env vars (do NOT store secrets in values.yaml). + WP_ENV: production + WP_HOME: https://wp.example.com + WP_SITEURL: https://wp.example.com/wp + + # Production guidance: disable WP-Cron and run a real scheduler. + DISABLE_WP_CRON: "true" + + # Production hardening (can be overridden for non-prod). + DISALLOW_FILE_MODS: "true" + WP_CACHE: "false" + WP_DEFAULT_THEME: starter-theme + + # Optional Redis object cache (requires a Redis cache plugin) + WP_REDIS_HOST: "" + WP_REDIS_PORT: "6379" + + # DB non-secret parts (password must come from a Secret) + DB_HOST: "" + DB_NAME: "" + DB_USER: "" + DB_PREFIX: wp_ + + extra: [] + # extra: + # - name: SOME_VAR + # value: "some-value" + +config: + # If set, chart will use this existing ConfigMap for env vars instead of creating one. + existingConfigMap: "" + +secret: + # Prefer External Secrets / SealedSecrets. This chart supports either: + # - referencing an existing Secret, or + # - creating a Secret from values (not recommended for real environments). + existingSecret: "" + create: false + data: {} + # data: + # DB_PASSWORD: "..." + # AUTH_KEY: "..." + # SECURE_AUTH_KEY: "..." + # LOGGED_IN_KEY: "..." + # NONCE_KEY: "..." + # AUTH_SALT: "..." + # SECURE_AUTH_SALT: "..." + # LOGGED_IN_SALT: "..." + # NONCE_SALT: "..." + +uploads: + persistence: + enabled: false + existingClaim: "" + storageClass: "" + accessModes: ["ReadWriteOnce"] + size: 10Gi + +cron: + enabled: false + schedule: "*/5 * * * *" + image: + repository: curlimages/curl + tag: "8.6.0" + urlPath: "/wp/wp-cron.php?doing_wp_cron" + From 32d7da0d1cf5b6555fc1759a5d8b0726e9271eb2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 08:02:46 +0000 Subject: [PATCH 05/29] chore: add CI security gates and container publishing Co-authored-by: Manuel H. --- .env.example | 4 + .github/dependabot.yml | 17 ++++ .github/workflows/ci.yml | 39 +++++++ .github/workflows/codeql.yml | 38 +++++++ .github/workflows/container-build.yml | 129 ++++++++++++++++++++++++ .github/workflows/dependency-review.yml | 17 ++++ CONTRIBUTING.md | 23 +++++ Makefile | 14 ++- README.md | 11 +- composer.json | 4 +- composer.lock | 2 +- 11 files changed, 291 insertions(+), 7 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/container-build.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 CONTRIBUTING.md diff --git a/.env.example b/.env.example index 43bf9f9..aaeacd0 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,7 @@ DB_USER=wordpress DB_PASSWORD=wordpress DB_HOST=db DB_PREFIX=wp_ +DB_ROOT_PASSWORD=root # ----------------------------------------------------------------------------- # Authentication Keys and Salts @@ -46,3 +47,6 @@ WP_CACHE=false WP_REDIS_HOST=redis WP_REDIS_PORT=6379 +# Optional: set the default theme (committed placeholder theme is `starter-theme`) +WP_DEFAULT_THEME=starter-theme + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7063f03 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..70712f2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + pull_request: + push: + branches: ["main"] + +permissions: + contents: read + +jobs: + php: + name: Composer validate / lint / audit + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + tools: composer:v2 + coverage: none + extensions: intl, mbstring, curl, zip + ini-values: memory_limit=512M + + - name: Composer validate + run: composer validate --strict + + - name: Composer install (dev) + run: composer install --no-interaction --no-progress + + - name: Lint (Pint) + run: composer lint + + - name: Composer audit + run: composer audit + diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..67b2b5d --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,38 @@ +name: CodeQL + +on: + push: + branches: ["main"] + pull_request: + schedule: + - cron: "23 3 * * 1" + +permissions: + actions: read + contents: read + security-events: write + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + language: ["php"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + diff --git a/.github/workflows/container-build.yml b/.github/workflows/container-build.yml new file mode 100644 index 0000000..2368113 --- /dev/null +++ b/.github/workflows/container-build.yml @@ -0,0 +1,129 @@ +name: Container Build + +on: + push: + branches: ["**"] + pull_request: + workflow_dispatch: + +concurrency: + group: container-build-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_PHP: ${{ github.repository }}-php + IMAGE_NGINX: ${{ github.repository }}-nginx + +permissions: + contents: read + packages: write + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata (php) + id: meta_php + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_PHP }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,format=short + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build (php) + id: build_php + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + target: php-runtime + push: ${{ github.event_name != 'pull_request' }} + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta_php.outputs.tags }} + labels: ${{ steps.meta_php.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + sbom: true + provenance: true + + - name: Docker metadata (nginx) + id: meta_nginx + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NGINX }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,format=short + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build (nginx) + id: build_nginx + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + target: nginx-runtime + push: ${{ github.event_name != 'pull_request' }} + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta_nginx.outputs.tags }} + labels: ${{ steps.meta_nginx.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + sbom: true + provenance: true + + - name: Trivy scan (php image) + if: github.event_name != 'pull_request' + uses: aquasecurity/trivy-action@0.33.1 + with: + image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_PHP }}@${{ steps.build_php.outputs.digest }} + format: table + exit-code: "1" + ignore-unfixed: true + vuln-type: os,library + severity: CRITICAL + + - name: Trivy scan (nginx image) + if: github.event_name != 'pull_request' + uses: aquasecurity/trivy-action@0.33.1 + with: + image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NGINX }}@${{ steps.build_nginx.outputs.digest }} + format: table + exit-code: "1" + ignore-unfixed: true + vuln-type: os,library + severity: CRITICAL + + - name: Write digests to summary + if: github.event_name != 'pull_request' + run: | + { + echo "## Image digests" + echo "" + echo "- PHP: \`${{ env.REGISTRY }}/${{ env.IMAGE_PHP }}@${{ steps.build_php.outputs.digest }}\`" + echo "- Nginx: \`${{ env.REGISTRY }}/${{ env.IMAGE_NGINX }}@${{ steps.build_nginx.outputs.digest }}\`" + } >> "$GITHUB_STEP_SUMMARY" + diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..3768dcb --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,17 @@ +name: Dependency Review + +on: + pull_request: + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Dependency Review + uses: actions/dependency-review-action@v4 + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4dc42c7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contributing + +Thanks for contributing! + +## Guidelines + +- **No secrets**: do not commit `.env`, API keys, private certs, or credentials. +- **Keep it generic**: this is a public boilerplate; avoid organization-specific references. +- **Production-minded defaults**: prefer least privilege, read-only root filesystems, and supply-chain hygiene. + +## Workflow (trunk-based) + +- Create a short-lived branch from `main` +- Keep changes small and focused +- Open a PR early +- Merge back to `main` frequently + +The CI builds branch-scoped container images and (on `main`) produces stable images intended to be deployed by digest. + +## Local Development + +See the repository README for Docker Compose commands. + diff --git a/Makefile b/Makefile index 3591ba8..d3c6df4 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,9 @@ SHELL := /usr/bin/env bash COMPOSE ?= docker compose +RUN_USER ?= $(shell id -u 2>/dev/null || echo 1000):$(shell id -g 2>/dev/null || echo 1000) -.PHONY: help up down restart ps logs shell up-mail up-dbadmin up-observability wp composer +.PHONY: help install composer-install up down restart ps logs shell up-mail up-dbadmin up-observability wp composer help: ## Show available targets @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_.-]+:.*##/ {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -10,6 +11,10 @@ help: ## Show available targets up: ## Start dev stack (build + up) $(COMPOSE) up -d --build +install: ## Install deps (Composer) then start stack + $(MAKE) composer-install + $(MAKE) up + down: ## Stop dev stack $(COMPOSE) down --remove-orphans @@ -35,8 +40,11 @@ up-observability: ## Start stack with exporters profile $(COMPOSE) --profile observability up -d --build wp: ## Run WP-CLI (example: make wp ARGS="core version") - $(COMPOSE) --profile tools run --rm wp $(ARGS) + $(COMPOSE) --profile tools run --rm --user "$(RUN_USER)" wp $(ARGS) composer: ## Run Composer in a container (example: make composer ARGS="install") - $(COMPOSE) --profile tools run --rm composer $(ARGS) + $(COMPOSE) --profile tools run --rm --user "$(RUN_USER)" composer $(ARGS) + +composer-install: ## Install Composer deps into the working tree + $(COMPOSE) --profile tools run --rm --user "$(RUN_USER)" composer install --no-interaction --no-progress diff --git a/README.md b/README.md index 07bb47b..1f3ba26 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ A production-minded WordPress boilerplate based on **[Roots Bedrock](https://roo ```bash cp .env.example .env -make up +make install ``` Then open: @@ -60,6 +60,15 @@ This boilerplate provides: Images are intended to be built in CI and deployed **by digest** (build once, deploy everywhere). +### Branch-based images + +The included GitHub Actions workflow builds and publishes images for each branch: + +- `ghcr.io//-php:` +- `ghcr.io//-nginx:` + +For production, prefer pinning by **digest** instead of tags. + --- ## Deploy to Kubernetes (Helm) diff --git a/composer.json b/composer.json index 681e5af..7e6dfaf 100644 --- a/composer.json +++ b/composer.json @@ -28,8 +28,8 @@ "oscarotero/env": "^2.1", "roots/bedrock-autoloader": "^1.0", "roots/bedrock-disallow-indexing": "^2.0", - "roots/wordpress": "6.9.1", - "roots/wp-config": "1.0.0", + "roots/wordpress": "^6.9.1", + "roots/wp-config": "^1.0.0", "vlucas/phpdotenv": "^5.6" }, "require-dev": { diff --git a/composer.lock b/composer.lock index c400dd1..1ff30b6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a863691fb8e2dfe0e7ef69cae6c32f77", + "content-hash": "22ffc1078e6fe872ac93fd19e6532ef8", "packages": [ { "name": "composer/installers", From b0b36375463b849763a4826cd1e2659b82436daf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 08:07:53 +0000 Subject: [PATCH 06/29] chore: scope Pint linting to project code Co-authored-by: Manuel H. --- pint.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 pint.json diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..e3bc448 --- /dev/null +++ b/pint.json @@ -0,0 +1,11 @@ +{ + "preset": "laravel", + "exclude": [ + "vendor", + "web/wp", + "web/app/mu-plugins/*/", + "web/app/plugins", + "web/app/uploads" + ] +} + From e8565ac35fb6d0fd91c53696e3c3e28bd6e7c718 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 08:09:33 +0000 Subject: [PATCH 07/29] chore: configure Pint and format project PHP Co-authored-by: Manuel H. --- config/application.php | 1 - config/environments/development.php | 1 - config/environments/production.php | 1 - config/environments/staging.php | 1 - pint.json | 4 ++-- web/app/themes/starter-theme/functions.php | 1 - web/app/themes/starter-theme/index.php | 1 - web/index.php | 1 - web/wp-config.php | 1 - 9 files changed, 2 insertions(+), 10 deletions(-) diff --git a/config/application.php b/config/application.php index 2dbb3a5..5fba72e 100644 --- a/config/application.php +++ b/config/application.php @@ -181,4 +181,3 @@ if (!defined('ABSPATH')) { define('ABSPATH', $webroot_dir . '/wp/'); } - diff --git a/config/environments/development.php b/config/environments/development.php index 27ac8c5..b14e803 100644 --- a/config/environments/development.php +++ b/config/environments/development.php @@ -20,4 +20,3 @@ // Enable plugin and theme updates and installation from the admin in dev. Config::define('DISALLOW_FILE_MODS', false); - diff --git a/config/environments/production.php b/config/environments/production.php index 1310089..e0f1205 100644 --- a/config/environments/production.php +++ b/config/environments/production.php @@ -8,4 +8,3 @@ Config::define('WP_DEBUG', false); Config::define('DISALLOW_INDEXING', false); - diff --git a/config/environments/staging.php b/config/environments/staging.php index 1943462..bc9b901 100644 --- a/config/environments/staging.php +++ b/config/environments/staging.php @@ -13,4 +13,3 @@ */ Config::define('DISALLOW_INDEXING', true); - diff --git a/pint.json b/pint.json index e3bc448..226b242 100644 --- a/pint.json +++ b/pint.json @@ -1,9 +1,9 @@ { - "preset": "laravel", + "preset": "psr12", "exclude": [ "vendor", "web/wp", - "web/app/mu-plugins/*/", + "web/app/mu-plugins", "web/app/plugins", "web/app/uploads" ] diff --git a/web/app/themes/starter-theme/functions.php b/web/app/themes/starter-theme/functions.php index fa5d258..dc57eed 100644 --- a/web/app/themes/starter-theme/functions.php +++ b/web/app/themes/starter-theme/functions.php @@ -10,4 +10,3 @@ add_theme_support('title-tag'); add_theme_support('post-thumbnails'); }); - diff --git a/web/app/themes/starter-theme/index.php b/web/app/themes/starter-theme/index.php index 54177bf..618e304 100644 --- a/web/app/themes/starter-theme/index.php +++ b/web/app/themes/starter-theme/index.php @@ -22,4 +22,3 @@ Date: Wed, 11 Feb 2026 08:12:08 +0000 Subject: [PATCH 08/29] docs: expand quickstart and 12-factor configuration Co-authored-by: Manuel H. --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 1f3ba26..f6baab8 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,12 @@ Then open: - http://localhost:8080 +Complete the WordPress installer in the browser, or (optional) use WP-CLI: + +```bash +make wp ARGS="core install --url=http://localhost:8080 --title=Boilerplate --admin_user=admin --admin_password=admin --admin_email=admin@example.com --skip-email" +``` + To stop: ```bash @@ -71,6 +77,17 @@ For production, prefer pinning by **digest** instead of tags. --- +## Configuration (12-factor) + +- Local dev uses a `.env` file (see `.env.example`). +- Production should set env vars via **Kubernetes Secrets/ConfigMaps** (do not bake secrets into images). + +Common toggles: + +- `DISABLE_WP_CRON=true` (production) +- `DISALLOW_FILE_MODS=true` (production hardening) +- `WP_CACHE=true` + `WP_REDIS_HOST=...` (optional Redis object cache; plugin required) + ## Deploy to Kubernetes (Helm) Helm chart: `helm/wp-boilerplate` From 7b22e9507aa1d2af4cd8c0d92b583c9c5f3d3cee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 08:16:44 +0000 Subject: [PATCH 09/29] fix: deny PHP execution in uploads and cache Co-authored-by: Manuel H. --- docker/nginx/conf.d/default.conf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker/nginx/conf.d/default.conf b/docker/nginx/conf.d/default.conf index 9e08fa2..20bd802 100644 --- a/docker/nginx/conf.d/default.conf +++ b/docker/nginx/conf.d/default.conf @@ -43,6 +43,11 @@ server { try_files $uri =404; } + # Never execute PHP from user-writable directories. + location ~* ^/app/(?:uploads|cache)/.*\.php$ { + deny all; + } + # WordPress / Bedrock front controller. location / { try_files $uri $uri/ /index.php?$args; From ae7063916541cc29c9a689856502947b68c8ece9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 08:18:19 +0000 Subject: [PATCH 10/29] chore: tighten nginx defaults Co-authored-by: Manuel H. --- docker/nginx/conf.d/default.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/nginx/conf.d/default.conf b/docker/nginx/conf.d/default.conf index 20bd802..93d2280 100644 --- a/docker/nginx/conf.d/default.conf +++ b/docker/nginx/conf.d/default.conf @@ -4,6 +4,8 @@ server { root /var/www/html/web; index index.php; + server_tokens off; + client_max_body_size 64m; include /etc/nginx/snippets/security-headers.conf; @@ -57,6 +59,7 @@ server { location ~ \.php$ { try_files $uri =404; include fastcgi_params; + fastcgi_param HTTP_PROXY ""; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_index index.php; From c2439b4b50bdb67b7ad99e79727acab322289a91 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 08:21:53 +0000 Subject: [PATCH 11/29] perf: speed up dev images by skipping composer build Co-authored-by: Manuel H. --- Dockerfile | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6cebb1d..10bb5f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -97,32 +97,56 @@ EXPOSE 9000 CMD ["php-fpm", "-F"] # ----------------------------------------------------------------------------- -# PHP dev (includes Composer + dev-friendly OPcache) +# PHP dev (no app baked; intended for bind-mount local source) # ----------------------------------------------------------------------------- -FROM php-runtime AS php-dev +FROM php-base AS php-dev -USER root -COPY --from=composer /usr/bin/composer /usr/local/bin/composer +WORKDIR /var/www/html + +RUN addgroup -g 10001 -S app && adduser -u 10001 -S -G app app + +RUN rm -f /usr/local/etc/php-fpm.d/www.conf +COPY docker/php/fpm-pool.conf /usr/local/etc/php-fpm.d/zz-app.conf + +COPY docker/php/php.ini /usr/local/etc/php/php.ini +COPY docker/php/conf.d/50-apcu.ini /usr/local/etc/php/conf.d/50-apcu.ini COPY docker/php/conf.d/99-opcache-dev.ini /usr/local/etc/php/conf.d/99-opcache.ini +COPY --from=composer /usr/bin/composer /usr/local/bin/composer + +RUN mkdir -p /tmp /var/run/php /var/www/html/web/app/uploads /var/www/html/web/app/cache && \ + chown -R app:app /var/run/php /var/www/html/web/app/uploads /var/www/html/web/app/cache + USER app +EXPOSE 9000 + +CMD ["php-fpm", "-F"] + # ----------------------------------------------------------------------------- -# Nginx runtime (non-root) +# Nginx base (non-root) # ----------------------------------------------------------------------------- -FROM nginxinc/nginx-unprivileged:${NGINX_VERSION}-alpine AS nginx-runtime +FROM nginxinc/nginx-unprivileged:${NGINX_VERSION}-alpine AS nginx-base WORKDIR /var/www/html USER root COPY docker/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf COPY docker/nginx/snippets/ /etc/nginx/snippets/ -COPY --from=build --chown=101:101 /var/www/html/web /var/www/html/web USER 101 EXPOSE 8080 # ----------------------------------------------------------------------------- -# Nginx dev (same image; source is bind-mounted in Compose) +# Nginx runtime (bakes Bedrock web/ for immutable deployments) +# ----------------------------------------------------------------------------- +FROM nginx-base AS nginx-runtime + +USER root +COPY --from=build --chown=101:101 /var/www/html/web /var/www/html/web +USER 101 + +# ----------------------------------------------------------------------------- +# Nginx dev (no app baked; intended for bind-mount local source) # ----------------------------------------------------------------------------- -FROM nginx-runtime AS nginx-dev +FROM nginx-base AS nginx-dev From f5ad5ab2e1474b3d7516519496b3c72b4c62c655 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 10:18:38 +0000 Subject: [PATCH 12/29] docs: add External Secrets and SealedSecrets Kubernetes examples Co-authored-by: Manuel H. --- README.md | 9 +++ docs/kubernetes/README.md | 23 ++++++++ docs/kubernetes/external-secrets/README.md | 22 +++++++ ...ecretstore-aws-secretsmanager.example.yaml | 21 +++++++ .../externalsecret-wordpress.example.yaml | 27 +++++++++ .../helm-values-production.example.yaml | 59 +++++++++++++++++++ docs/kubernetes/sealed-secrets/README.md | 19 ++++++ .../sealedsecret-wordpress.example.yaml | 36 +++++++++++ docs/kubernetes/secrets-required.md | 50 ++++++++++++++++ helm/wp-boilerplate/README.md | 6 ++ 10 files changed, 272 insertions(+) create mode 100644 docs/kubernetes/README.md create mode 100644 docs/kubernetes/external-secrets/README.md create mode 100644 docs/kubernetes/external-secrets/clustersecretstore-aws-secretsmanager.example.yaml create mode 100644 docs/kubernetes/external-secrets/externalsecret-wordpress.example.yaml create mode 100644 docs/kubernetes/helm-values-production.example.yaml create mode 100644 docs/kubernetes/sealed-secrets/README.md create mode 100644 docs/kubernetes/sealed-secrets/sealedsecret-wordpress.example.yaml create mode 100644 docs/kubernetes/secrets-required.md diff --git a/README.md b/README.md index f6baab8..f0ac30a 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,15 @@ In production, you should disable the built-in pseudo-cron and run a real schedu - Set `DISABLE_WP_CRON=true` - Use a Kubernetes `CronJob` (template included) or an external scheduler to trigger cron processing +## Secrets on Kubernetes + +Recommended approaches: + +- External Secrets Operator (ESO) +- SealedSecrets + +See `docs/kubernetes/` for templates and required keys. + --- ## License diff --git a/docs/kubernetes/README.md b/docs/kubernetes/README.md new file mode 100644 index 0000000..5e6648d --- /dev/null +++ b/docs/kubernetes/README.md @@ -0,0 +1,23 @@ +# Kubernetes Deployment Notes + +This boilerplate is designed to be deployed to Kubernetes via Helm: + +- Chart: `helm/wp-boilerplate` +- Prefer **build once, deploy everywhere** by pinning images **by digest** +- Prefer **object storage** for uploads (S3-compatible) to avoid shared PVCs + +## Secrets management (recommended) + +Do not commit secrets to git. + +Recommended approaches: + +1. **External Secrets Operator (ESO)** (pull from a secret manager at runtime) +2. **SealedSecrets** (encrypt Secret manifests for safe storage in git) + +See: + +- `docs/kubernetes/secrets-required.md` +- `docs/kubernetes/external-secrets/` +- `docs/kubernetes/sealed-secrets/` + diff --git a/docs/kubernetes/external-secrets/README.md b/docs/kubernetes/external-secrets/README.md new file mode 100644 index 0000000..3c6c096 --- /dev/null +++ b/docs/kubernetes/external-secrets/README.md @@ -0,0 +1,22 @@ +# External Secrets Operator (ESO) Examples + +These examples assume you are using: + +- https://external-secrets.io/ + +They are intentionally templates with placeholders. + +Typical flow: + +1. Install ESO in your cluster +2. Configure a `SecretStore` / `ClusterSecretStore` for your provider +3. Create an `ExternalSecret` that materializes a Kubernetes Secret containing: + - `DB_PASSWORD` + - WordPress salts/keys +4. Point Helm to that Secret using `secret.existingSecret` + +Files: + +- `clustersecretstore-aws-secretsmanager.example.yaml` (template) +- `externalsecret-wordpress.example.yaml` (template) + diff --git a/docs/kubernetes/external-secrets/clustersecretstore-aws-secretsmanager.example.yaml b/docs/kubernetes/external-secrets/clustersecretstore-aws-secretsmanager.example.yaml new file mode 100644 index 0000000..54a95d0 --- /dev/null +++ b/docs/kubernetes/external-secrets/clustersecretstore-aws-secretsmanager.example.yaml @@ -0,0 +1,21 @@ +# Template example for AWS Secrets Manager. +# Replace placeholders and adjust auth to match your environment (IRSA, access keys, etc). +# +# Docs: +# https://external-secrets.io/latest/provider/aws-secrets-manager/ + +apiVersion: external-secrets.io/v1beta1 +kind: ClusterSecretStore +metadata: + name: aws-secretsmanager +spec: + provider: + aws: + service: SecretsManager + region: us-east-1 + auth: + jwt: + serviceAccountRef: + name: external-secrets + namespace: external-secrets + diff --git a/docs/kubernetes/external-secrets/externalsecret-wordpress.example.yaml b/docs/kubernetes/external-secrets/externalsecret-wordpress.example.yaml new file mode 100644 index 0000000..a152aaa --- /dev/null +++ b/docs/kubernetes/external-secrets/externalsecret-wordpress.example.yaml @@ -0,0 +1,27 @@ +# Template example that materializes a Kubernetes Secret with Bedrock/WordPress keys. +# +# Assumptions: +# - You already created/configured a ClusterSecretStore (example: aws-secretsmanager) +# - Your secret manager contains a JSON document with these fields +# +# Docs: +# https://external-secrets.io/latest/api/externalsecret/ + +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: wp-secrets + namespace: wp +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: aws-secretsmanager + target: + name: wp-secrets + creationPolicy: Owner + dataFrom: + - extract: + # Example: ARN or secret name in your provider + key: REPLACE_ME/wordpress/production + diff --git a/docs/kubernetes/helm-values-production.example.yaml b/docs/kubernetes/helm-values-production.example.yaml new file mode 100644 index 0000000..bf06e49 --- /dev/null +++ b/docs/kubernetes/helm-values-production.example.yaml @@ -0,0 +1,59 @@ +# Example values override for a real cluster. +# +# Usage: +# helm upgrade --install wp ./helm/wp-boilerplate \ +# -n wp --create-namespace \ +# -f docs/kubernetes/helm-values-production.example.yaml + +image: + php: + repository: ghcr.io/OWNER/REPO-php + digest: sha256:REPLACE_ME + web: + repository: ghcr.io/OWNER/REPO-nginx + digest: sha256:REPLACE_ME + +ingress: + enabled: true + className: nginx + annotations: {} + hosts: + - host: wp.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: wp-tls + hosts: + - wp.example.com + +env: + WP_ENV: production + WP_HOME: https://wp.example.com + WP_SITEURL: https://wp.example.com/wp + + # Production guidance + DISABLE_WP_CRON: "true" + DISALLOW_FILE_MODS: "true" + + # DB connection (password is stored in Secret) + DB_HOST: my-db.example.com:3306 + DB_NAME: wordpress + DB_USER: wordpress + +config: + existingConfigMap: "" # or set to an existing ConfigMap name + +secret: + existingSecret: wp-secrets + +# Recommended: offload uploads to S3-compatible object storage. +uploads: + persistence: + enabled: false + +# Optional: run WP-Cron via a real scheduler. +cron: + enabled: true + schedule: "*/5 * * * *" + diff --git a/docs/kubernetes/sealed-secrets/README.md b/docs/kubernetes/sealed-secrets/README.md new file mode 100644 index 0000000..59f28e1 --- /dev/null +++ b/docs/kubernetes/sealed-secrets/README.md @@ -0,0 +1,19 @@ +# SealedSecrets Examples + +These templates assume you are using Bitnami SealedSecrets: + +- https://github.com/bitnami-labs/sealed-secrets + +## Typical flow + +1. Install the Sealed Secrets controller in your cluster +2. Create a normal Secret locally (never commit it) +3. Use `kubeseal` to encrypt it into a `SealedSecret` +4. Commit the `SealedSecret` manifest to git + +This chart can then reference the created Secret via `secret.existingSecret`. + +Files: + +- `sealedsecret-wordpress.example.yaml` (template) + diff --git a/docs/kubernetes/sealed-secrets/sealedsecret-wordpress.example.yaml b/docs/kubernetes/sealed-secrets/sealedsecret-wordpress.example.yaml new file mode 100644 index 0000000..b6f89ed --- /dev/null +++ b/docs/kubernetes/sealed-secrets/sealedsecret-wordpress.example.yaml @@ -0,0 +1,36 @@ +# Template SealedSecret. +# +# IMPORTANT: +# - Do not put real plaintext secrets in git. +# - Use kubeseal to generate encryptedData values for your cluster/controller. +# +# Example: +# kubectl -n wp create secret generic wp-secrets \ +# --from-literal=DB_PASSWORD='...' \ +# --from-literal=AUTH_KEY='...' \ +# ... \ +# --dry-run=client -o yaml \ +# | kubeseal --format yaml > sealedsecret-wordpress.yaml + +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + name: wp-secrets + namespace: wp +spec: + encryptedData: + DB_PASSWORD: REPLACE_WITH_KUBESEAL_OUTPUT + AUTH_KEY: REPLACE_WITH_KUBESEAL_OUTPUT + SECURE_AUTH_KEY: REPLACE_WITH_KUBESEAL_OUTPUT + LOGGED_IN_KEY: REPLACE_WITH_KUBESEAL_OUTPUT + NONCE_KEY: REPLACE_WITH_KUBESEAL_OUTPUT + AUTH_SALT: REPLACE_WITH_KUBESEAL_OUTPUT + SECURE_AUTH_SALT: REPLACE_WITH_KUBESEAL_OUTPUT + LOGGED_IN_SALT: REPLACE_WITH_KUBESEAL_OUTPUT + NONCE_SALT: REPLACE_WITH_KUBESEAL_OUTPUT + template: + metadata: + name: wp-secrets + namespace: wp + type: Opaque + diff --git a/docs/kubernetes/secrets-required.md b/docs/kubernetes/secrets-required.md new file mode 100644 index 0000000..fb4ffbf --- /dev/null +++ b/docs/kubernetes/secrets-required.md @@ -0,0 +1,50 @@ +# Required Secrets / Env Vars (Bedrock) + +This boilerplate follows Bedrock's environment-variable configuration model. + +## Required env vars (ConfigMap) + +Typically stored in a ConfigMap (non-secret): + +- `WP_ENV` +- `WP_HOME` +- `WP_SITEURL` +- `DB_HOST` +- `DB_NAME` +- `DB_USER` +- `DB_PREFIX` (optional) + +## Required secret keys (Secret) + +Store these in a Kubernetes Secret (or via External Secrets / SealedSecrets): + +### Database + +- `DB_PASSWORD` + +### WordPress salts/keys + +- `AUTH_KEY` +- `SECURE_AUTH_KEY` +- `LOGGED_IN_KEY` +- `NONCE_KEY` +- `AUTH_SALT` +- `SECURE_AUTH_SALT` +- `LOGGED_IN_SALT` +- `NONCE_SALT` + +Generate secure salts: + +- https://roots.io/salts.html + +## Optional secret keys + +- `WP_REDIS_PASSWORD` (if your Redis requires auth) + +## Helm chart integration + +The Helm chart supports: + +- `secret.existingSecret`: reference a Secret created elsewhere (recommended) +- `secret.create=true` + `secret.data`: creates a Secret from Helm values (not recommended for real environments) + diff --git a/helm/wp-boilerplate/README.md b/helm/wp-boilerplate/README.md index 5b02b13..2909024 100644 --- a/helm/wp-boilerplate/README.md +++ b/helm/wp-boilerplate/README.md @@ -37,6 +37,12 @@ You can either: - Provide `secret.existingSecret`, or - Set `secret.create=true` and populate `secret.data` (not recommended) +Examples: + +- `docs/kubernetes/external-secrets/` +- `docs/kubernetes/sealed-secrets/` +- `docs/kubernetes/secrets-required.md` + Expected Secret keys include: - `DB_PASSWORD` From 5ad5fcbdc045a02133783f94925ba9c1ae1a0d13 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 10:57:25 +0000 Subject: [PATCH 13/29] fix: align non-root runtime permissions and health endpoints Co-authored-by: Manuel H. --- Dockerfile | 19 +++++++++++++++++-- compose.prod.yaml | 2 +- docker/nginx/conf.d/default.conf | 4 ++-- docker/php/fpm-pool.conf | 4 +++- helm/wp-boilerplate/values.yaml | 4 ++++ 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 10bb5f8..e25dc0c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -70,7 +70,14 @@ FROM php-base AS php-runtime WORKDIR /var/www/html -RUN addgroup -g 10001 -S app && adduser -u 10001 -S -G app app +RUN set -eux; \ + addgroup -g 10001 -S app; \ + adduser -u 10001 -S -G app app; \ + # Ensure the PHP user can create an FPM socket readable by the nginx-unprivileged + # container (uid/gid 101). + group101="$(awk -F: '$3==101{print $1; exit}' /etc/group)"; \ + if [ -z "$group101" ]; then addgroup -g 101 -S web; group101="web"; fi; \ + addgroup app "$group101" || true # Replace the default pool with our socket-based pool. RUN rm -f /usr/local/etc/php-fpm.d/www.conf @@ -101,9 +108,17 @@ CMD ["php-fpm", "-F"] # ----------------------------------------------------------------------------- FROM php-base AS php-dev +ARG APP_UID=1000 +ARG APP_GID=1000 + WORKDIR /var/www/html -RUN addgroup -g 10001 -S app && adduser -u 10001 -S -G app app +RUN set -eux; \ + addgroup -g "${APP_GID}" -S app; \ + adduser -u "${APP_UID}" -S -G app app; \ + group101="$(awk -F: '$3==101{print $1; exit}' /etc/group)"; \ + if [ -z "$group101" ]; then addgroup -g 101 -S web; group101="web"; fi; \ + addgroup app "$group101" || true RUN rm -f /usr/local/etc/php-fpm.d/www.conf COPY docker/php/fpm-pool.conf /usr/local/etc/php-fpm.d/zz-app.conf diff --git a/compose.prod.yaml b/compose.prod.yaml index b9a2948..b77dc22 100644 --- a/compose.prod.yaml +++ b/compose.prod.yaml @@ -12,6 +12,7 @@ services: - ALL tmpfs: - /tmp:rw,noexec,nosuid,size=64m + - /var/cache/nginx:rw,nosuid,size=32m volumes: - php-socket:/var/run/php - uploads:/var/www/html/web/app/uploads @@ -31,7 +32,6 @@ services: - ALL tmpfs: - /tmp:rw,noexec,nosuid,size=128m - - /var/run/php:rw,nosuid,size=8m volumes: - php-socket:/var/run/php - uploads:/var/www/html/web/app/uploads diff --git a/docker/nginx/conf.d/default.conf b/docker/nginx/conf.d/default.conf index 93d2280..85a03bf 100644 --- a/docker/nginx/conf.d/default.conf +++ b/docker/nginx/conf.d/default.conf @@ -71,16 +71,16 @@ server { location = /ping { access_log off; include fastcgi_params; + fastcgi_param HTTP_PROXY ""; fastcgi_param SCRIPT_FILENAME $document_root/index.php; - fastcgi_param SCRIPT_NAME /index.php; fastcgi_pass unix:/var/run/php/php-fpm.sock; } location = /status { access_log off; include fastcgi_params; + fastcgi_param HTTP_PROXY ""; fastcgi_param SCRIPT_FILENAME $document_root/index.php; - fastcgi_param SCRIPT_NAME /index.php; fastcgi_pass unix:/var/run/php/php-fpm.sock; allow 127.0.0.1; allow 10.0.0.0/8; diff --git a/docker/php/fpm-pool.conf b/docker/php/fpm-pool.conf index 5e03d3c..6f5e8f9 100644 --- a/docker/php/fpm-pool.conf +++ b/docker/php/fpm-pool.conf @@ -4,7 +4,9 @@ group = app listen = /var/run/php/php-fpm.sock listen.owner = app -listen.group = app +; Nginx unprivileged image runs as uid/gid 101. Set the socket group to 101 so +; the web container can connect without elevating privileges. +listen.group = 101 listen.mode = 0660 pm = dynamic diff --git a/helm/wp-boilerplate/values.yaml b/helm/wp-boilerplate/values.yaml index 3f42936..047366d 100644 --- a/helm/wp-boilerplate/values.yaml +++ b/helm/wp-boilerplate/values.yaml @@ -19,6 +19,10 @@ serviceAccount: podAnnotations: {} podSecurityContext: + # Required for non-root containers writing to emptyDir/PVC mounts. + # 101 matches the nginx-unprivileged uid/gid and is added as a supplementary + # group for the PHP user in the provided image. + fsGroup: 101 seccompProfile: type: RuntimeDefault From 09de85516dd19b9a307851b6c1a205e057c1daa7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 10:58:17 +0000 Subject: [PATCH 14/29] feat: add one-command local bootstrap and dev UID mapping Co-authored-by: Manuel H. --- .env.example | 13 ++++++++++++- .gitignore | 1 + Makefile | 42 ++++++++++++++++++++++++++++++++++++++++++ README.md | 14 +++++++++++--- compose.yaml | 11 ++++++++++- web/app/cache/.gitkeep | 1 + 6 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 web/app/cache/.gitkeep diff --git a/.env.example b/.env.example index aaeacd0..45adf0b 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,9 @@ WP_ENV=development WP_HOME=http://localhost:8080 -WP_SITEURL=${WP_HOME}/wp +# Keep WP_SITEURL explicit for compatibility with tooling that does not +# interpolate variables inside env files. +WP_SITEURL=http://localhost:8080/wp # ----------------------------------------------------------------------------- # Database (local dev defaults) @@ -50,3 +52,12 @@ WP_REDIS_PORT=6379 # Optional: set the default theme (committed placeholder theme is `starter-theme`) WP_DEFAULT_THEME=starter-theme +# ----------------------------------------------------------------------------- +# Docker dev ergonomics +# ----------------------------------------------------------------------------- +# +# Used to build the php-dev image with a UID/GID matching your host user so +# bind-mounts and generated files are writable without manual chmod/chown. +APP_UID=1000 +APP_GID=1000 + diff --git a/.gitignore b/.gitignore index 409ef05..04885f1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ web/app/upgrade web/app/uploads/* !web/app/uploads/.gitkeep web/app/cache/* +!web/app/cache/.gitkeep # WordPress (managed by Composer) web/wp diff --git a/Makefile b/Makefile index d3c6df4..72a901c 100644 --- a/Makefile +++ b/Makefile @@ -4,10 +4,14 @@ COMPOSE ?= docker compose RUN_USER ?= $(shell id -u 2>/dev/null || echo 1000):$(shell id -g 2>/dev/null || echo 1000) .PHONY: help install composer-install up down restart ps logs shell up-mail up-dbadmin up-observability wp composer +.PHONY: bootstrap env wait wp-install help: ## Show available targets @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_.-]+:.*##/ {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) +env: ## Create .env from .env.example if missing + @if [ -f .env ]; then echo ".env exists"; else cp .env.example .env && echo "Created .env from .env.example"; fi + up: ## Start dev stack (build + up) $(COMPOSE) up -d --build @@ -15,6 +19,26 @@ install: ## Install deps (Composer) then start stack $(MAKE) composer-install $(MAKE) up +wait: ## Wait for php-fpm ping via Nginx + @echo "Waiting for http://localhost:8080/ping ..." + @for i in $$(seq 1 60); do \ + if command -v curl >/dev/null 2>&1; then \ + curl -fsS http://localhost:8080/ping >/dev/null 2>&1 && echo "Ready" && exit 0; \ + else \ + wget -qO- http://localhost:8080/ping >/dev/null 2>&1 && echo "Ready" && exit 0; \ + fi; \ + sleep 1; \ + done; \ + echo "Timed out waiting for web/php"; \ + exit 1 + +bootstrap: ## One-command local bootstrap (env + deps + up + wait + wp-install) + $(MAKE) env + $(MAKE) composer-install + $(MAKE) up + $(MAKE) wait + $(MAKE) wp-install + down: ## Stop dev stack $(COMPOSE) down --remove-orphans @@ -48,3 +72,21 @@ composer: ## Run Composer in a container (example: make composer ARGS="install") composer-install: ## Install Composer deps into the working tree $(COMPOSE) --profile tools run --rm --user "$(RUN_USER)" composer install --no-interaction --no-progress +wp-install: ## Install WordPress if not already installed (local dev) + @set -euo pipefail; \ + if [ ! -f .env ]; then echo "Missing .env (run: make env)"; exit 1; fi; \ + set -a; . ./.env; set +a; \ + URL="$${WP_HOME:-http://localhost:8080}"; \ + TITLE="$${WP_SITE_TITLE:-Boilerplate}"; \ + ADMIN_USER="$${WP_ADMIN_USER:-admin}"; \ + ADMIN_PASSWORD="$${WP_ADMIN_PASSWORD:-admin}"; \ + ADMIN_EMAIL="$${WP_ADMIN_EMAIL:-admin@example.com}"; \ + $(COMPOSE) --profile tools run --rm --user "$(RUN_USER)" wp --path=web/wp core is-installed >/dev/null 2>&1 || \ + $(COMPOSE) --profile tools run --rm --user "$(RUN_USER)" wp --path=web/wp core install \ + --url="$$URL" \ + --title="$$TITLE" \ + --admin_user="$$ADMIN_USER" \ + --admin_password="$$ADMIN_PASSWORD" \ + --admin_email="$$ADMIN_EMAIL" \ + --skip-email + diff --git a/README.md b/README.md index f0ac30a..6742b47 100644 --- a/README.md +++ b/README.md @@ -22,17 +22,25 @@ A production-minded WordPress boilerplate based on **[Roots Bedrock](https://roo ```bash cp .env.example .env -make install +make bootstrap ``` Then open: - http://localhost:8080 -Complete the WordPress installer in the browser, or (optional) use WP-CLI: +`make bootstrap` also runs a WP-CLI install if the site isn't installed yet. +Defaults (override in `.env`): + +- `WP_SITE_TITLE` (default: `Boilerplate`) +- `WP_ADMIN_USER` (default: `admin`) +- `WP_ADMIN_PASSWORD` (default: `admin`) +- `WP_ADMIN_EMAIL` (default: `admin@example.com`) + +You can re-run: ```bash -make wp ARGS="core install --url=http://localhost:8080 --title=Boilerplate --admin_user=admin --admin_password=admin --admin_email=admin@example.com --skip-email" +make wp-install ``` To stop: diff --git a/compose.yaml b/compose.yaml index dfddad7..ab3b699 100644 --- a/compose.yaml +++ b/compose.yaml @@ -14,8 +14,10 @@ services: volumes: - ./:/var/www/html:ro - php-socket:/var/run/php + - uploads:/var/www/html/web/app/uploads + - cache:/var/www/html/web/app/cache healthcheck: - test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz >/dev/null 2>&1 || exit 1"] + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/ping >/dev/null 2>&1 || exit 1"] interval: 10s timeout: 3s retries: 10 @@ -28,6 +30,9 @@ services: context: . dockerfile: Dockerfile target: php-dev + args: + APP_UID: ${APP_UID:-1000} + APP_GID: ${APP_GID:-1000} env_file: - .env depends_on: @@ -38,6 +43,8 @@ services: volumes: - ./:/var/www/html - php-socket:/var/run/php + - uploads:/var/www/html/web/app/uploads + - cache:/var/www/html/web/app/cache # --------------------------------------------------------------------------- # Database @@ -161,4 +168,6 @@ volumes: db-data: redis-data: php-socket: + uploads: + cache: diff --git a/web/app/cache/.gitkeep b/web/app/cache/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/web/app/cache/.gitkeep @@ -0,0 +1 @@ + From 2edf60d10dce9295b96941b8e6ad369300a8088d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 10:59:07 +0000 Subject: [PATCH 15/29] chore(ci): add Helm lint and template checks Co-authored-by: Manuel H. --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70712f2..2b72ede 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,3 +37,27 @@ jobs: - name: Composer audit run: composer audit + helm: + name: Helm lint / template + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Helm + uses: azure/setup-helm@v4 + + - name: Helm lint + run: > + helm lint helm/wp-boilerplate + --set image.php.tag=ci + --set image.web.tag=ci + + - name: Helm template (render) + run: > + helm template wp helm/wp-boilerplate + --namespace wp + --set image.php.tag=ci + --set image.web.tag=ci + > /tmp/wp-boilerplate.rendered.yaml + From 92fe87c6f0fbc873d1afb5df1c8a520b77420583 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 11:09:19 +0000 Subject: [PATCH 16/29] chore(ci): sign published images with cosign Co-authored-by: Manuel H. --- .github/workflows/container-build.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/container-build.yml b/.github/workflows/container-build.yml index 2368113..9d69b30 100644 --- a/.github/workflows/container-build.yml +++ b/.github/workflows/container-build.yml @@ -117,6 +117,20 @@ jobs: vuln-type: os,library severity: CRITICAL + - name: Install Cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@v4.0.0 + with: + cosign-release: v3.0.4 + + - name: Cosign sign images (keyless) + if: github.event_name != 'pull_request' + env: + COSIGN_YES: "true" + run: | + cosign sign "${{ env.REGISTRY }}/${{ env.IMAGE_PHP }}@${{ steps.build_php.outputs.digest }}" + cosign sign "${{ env.REGISTRY }}/${{ env.IMAGE_NGINX }}@${{ steps.build_nginx.outputs.digest }}" + - name: Write digests to summary if: github.event_name != 'pull_request' run: | From 6ec9ba94434ee2e51a1850c0f0215b6cc8662821 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 11:10:34 +0000 Subject: [PATCH 17/29] docs: add S3 uploads offload guidance and templates Co-authored-by: Manuel H. --- README.md | 2 + config/application.php | 20 ++++++ docs/kubernetes/README.md | 1 + .../kubernetes/configmap-bedrock.example.yaml | 39 ++++++++++ docs/kubernetes/uploads-s3.md | 71 +++++++++++++++++++ helm/wp-boilerplate/README.md | 2 + 6 files changed, 135 insertions(+) create mode 100644 docs/kubernetes/configmap-bedrock.example.yaml create mode 100644 docs/kubernetes/uploads-s3.md diff --git a/README.md b/README.md index 6742b47..a0b3965 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,8 @@ Preferred (cloud-native): use an S3-compatible uploads plugin (no shared PVC req Alternative (simple clusters): enable the chart's `uploads.persistence` option to mount a PVC. +Details: `docs/kubernetes/uploads-s3.md` + --- ## WP-Cron (Production) diff --git a/config/application.php b/config/application.php index 5fba72e..c89c0e4 100644 --- a/config/application.php +++ b/config/application.php @@ -130,6 +130,26 @@ Config::define('WP_REDIS_DATABASE', env('WP_REDIS_DATABASE') ?: 0); Config::define('WP_REDIS_PREFIX', env('WP_REDIS_PREFIX') ?: $table_prefix); +// Uploads offload (S3-compatible) - plugin required (e.g. humanmade/s3-uploads). +if ($bucket = env('S3_UPLOADS_BUCKET')) { + Config::define('S3_UPLOADS_BUCKET', $bucket); + + foreach ([ + 'S3_UPLOADS_REGION', + 'S3_UPLOADS_BUCKET_URL', + 'S3_UPLOADS_KEY', + 'S3_UPLOADS_SECRET', + 'S3_UPLOADS_ENDPOINT', + 'S3_UPLOADS_PATH_STYLE_ENDPOINT', + 'S3_UPLOADS_USE_INSTANCE_PROFILE', + ] as $key) { + $value = env($key); + if ($value !== null && $value !== '') { + Config::define($key, $value); + } + } +} + /** * Custom settings / hardening */ diff --git a/docs/kubernetes/README.md b/docs/kubernetes/README.md index 5e6648d..d3c286c 100644 --- a/docs/kubernetes/README.md +++ b/docs/kubernetes/README.md @@ -20,4 +20,5 @@ See: - `docs/kubernetes/secrets-required.md` - `docs/kubernetes/external-secrets/` - `docs/kubernetes/sealed-secrets/` +- `docs/kubernetes/uploads-s3.md` diff --git a/docs/kubernetes/configmap-bedrock.example.yaml b/docs/kubernetes/configmap-bedrock.example.yaml new file mode 100644 index 0000000..369e603 --- /dev/null +++ b/docs/kubernetes/configmap-bedrock.example.yaml @@ -0,0 +1,39 @@ +# Example ConfigMap for Bedrock env vars (non-secret). +# +# Create your own and reference it via: +# config.existingConfigMap: wp-env +# +# NOTE: Do NOT put credentials or salts here. Use a Secret instead. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: wp-env + namespace: wp +data: + WP_ENV: "production" + WP_HOME: "https://wp.example.com" + WP_SITEURL: "https://wp.example.com/wp" + + DISABLE_WP_CRON: "true" + DISALLOW_FILE_MODS: "true" + WP_CACHE: "false" + WP_DEFAULT_THEME: "starter-theme" + + DB_HOST: "my-db.example.com:3306" + DB_NAME: "wordpress" + DB_USER: "wordpress" + DB_PREFIX: "wp_" + + # Optional: Redis object cache (plugin required) + WP_REDIS_HOST: "" + WP_REDIS_PORT: "6379" + + # Optional: S3 uploads offload (plugin required, e.g. humanmade/s3-uploads) + S3_UPLOADS_BUCKET: "my-bucket" + S3_UPLOADS_REGION: "us-east-1" + S3_UPLOADS_BUCKET_URL: "https://cdn.example.com" + S3_UPLOADS_ENDPOINT: "" + S3_UPLOADS_PATH_STYLE_ENDPOINT: "false" + S3_UPLOADS_USE_INSTANCE_PROFILE: "true" + diff --git a/docs/kubernetes/uploads-s3.md b/docs/kubernetes/uploads-s3.md new file mode 100644 index 0000000..2d8d012 --- /dev/null +++ b/docs/kubernetes/uploads-s3.md @@ -0,0 +1,71 @@ +# Uploads Offload to S3-Compatible Object Storage (Recommended) + +In Kubernetes, scaling WordPress beyond 1 replica usually requires avoiding a shared filesystem for uploads. + +Recommended approach: **offload uploads to an S3-compatible bucket**. + +## Plugin option: humanmade/s3-uploads + +One popular open-source option: + +- https://github.com/humanmade/S3-Uploads + +Install (Bedrock / Composer): + +```bash +composer require humanmade/s3-uploads +``` + +> The plugin reads configuration from PHP constants. This boilerplate can define +> those constants from environment variables (see `config/application.php`). + +## Env vars / constants (typical) + +At minimum: + +- `S3_UPLOADS_BUCKET` +- `S3_UPLOADS_REGION` (AWS) or omit for some S3-compatible providers + +Auth options: + +- Key/secret in a Secret: + - `S3_UPLOADS_KEY` + - `S3_UPLOADS_SECRET` +- Or workload identity / instance profile: + - `S3_UPLOADS_USE_INSTANCE_PROFILE=true` + +S3-compatible endpoints (MinIO, Ceph, etc): + +- `S3_UPLOADS_ENDPOINT=https://minio.example.com` +- `S3_UPLOADS_PATH_STYLE_ENDPOINT=true` + +Public bucket URL (CDN or direct): + +- `S3_UPLOADS_BUCKET_URL=https://cdn.example.com` + +## Helm integration + +1. Keep uploads PVC disabled: + +```yaml +uploads: + persistence: + enabled: false +``` + +2. Provide env vars via a ConfigMap/Secret you manage, then reference them: + +```yaml +config: + existingConfigMap: wp-env +secret: + existingSecret: wp-secrets +``` + +See also: + +- `docs/kubernetes/configmap-bedrock.example.yaml` +- `docs/kubernetes/secrets-required.md` +- `docs/kubernetes/external-secrets/` +- `docs/kubernetes/sealed-secrets/` + diff --git a/helm/wp-boilerplate/README.md b/helm/wp-boilerplate/README.md index 2909024..5a6f1ee 100644 --- a/helm/wp-boilerplate/README.md +++ b/helm/wp-boilerplate/README.md @@ -55,6 +55,8 @@ Expected Secret keys include: Use a uploads offload plugin (example: `humanmade/s3-uploads`) so you can scale replicas without shared storage. +See: `docs/kubernetes/uploads-s3.md` + ### Alternative: PVC Set: From 523ea3e044342280917b25b881427bcaf7dc2ff3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 12:10:38 +0000 Subject: [PATCH 18/29] feat: add optional local TLS via Caddy Co-authored-by: Manuel H. --- .env.example | 4 ++++ Makefile | 12 +++++++++++- README.md | 15 +++++++++++++++ compose.yaml | 19 +++++++++++++++++++ docker/caddy/Caddyfile | 18 ++++++++++++++++++ docker/nginx/conf.d/default.conf | 6 ++++++ 6 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 docker/caddy/Caddyfile diff --git a/.env.example b/.env.example index 45adf0b..0ea6fc0 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,10 @@ WP_HOME=http://localhost:8080 # interpolate variables inside env files. WP_SITEURL=http://localhost:8080/wp +# Optional TLS dev endpoint: +# WP_HOME=https://wp.localhost:8443 +# WP_SITEURL=https://wp.localhost:8443/wp + # ----------------------------------------------------------------------------- # Database (local dev defaults) # ----------------------------------------------------------------------------- diff --git a/Makefile b/Makefile index 72a901c..7c342fd 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ COMPOSE ?= docker compose RUN_USER ?= $(shell id -u 2>/dev/null || echo 1000):$(shell id -g 2>/dev/null || echo 1000) .PHONY: help install composer-install up down restart ps logs shell up-mail up-dbadmin up-observability wp composer -.PHONY: bootstrap env wait wp-install +.PHONY: bootstrap env wait wp-install up-tls bootstrap-tls help: ## Show available targets @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_.-]+:.*##/ {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -39,6 +39,16 @@ bootstrap: ## One-command local bootstrap (env + deps + up + wait + wp-install) $(MAKE) wait $(MAKE) wp-install +up-tls: ## Start dev stack + local TLS proxy (https://wp.localhost:8443) + $(COMPOSE) --profile tls up -d --build + +bootstrap-tls: ## Bootstrap stack + local TLS proxy (requires WP_HOME/WP_SITEURL set to https://wp.localhost:8443) + $(MAKE) env + $(MAKE) composer-install + $(MAKE) up-tls + $(MAKE) wait + $(MAKE) wp-install + down: ## Stop dev stack $(COMPOSE) down --remove-orphans diff --git a/README.md b/README.md index a0b3965..ac8708f 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,21 @@ To stop: make down ``` +### Optional: local HTTPS (TLS) + +This repo includes an optional Caddy reverse proxy for local HTTPS: + +```bash +# Update .env so WP_HOME/WP_SITEURL use https://wp.localhost:8443 +make bootstrap-tls +``` + +URL: + +- https://wp.localhost:8443 + +Note: Caddy uses a locally-generated certificate (browser will warn). For a trusted cert, use `mkcert` and configure Caddy with your generated certs. + ### Optional profiles (dev conveniences) ```bash diff --git a/compose.yaml b/compose.yaml index ab3b699..2e63db7 100644 --- a/compose.yaml +++ b/compose.yaml @@ -22,6 +22,23 @@ services: timeout: 3s retries: 10 + # --------------------------------------------------------------------------- + # Optional: TLS reverse proxy for local dev. + # URL: https://wp.localhost:8443 + # --------------------------------------------------------------------------- + caddy: + image: caddy:2-alpine + profiles: ["tls"] + ports: + - "8443:8443" + volumes: + - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + depends_on: + web: + condition: service_healthy + # --------------------------------------------------------------------------- # PHP-FPM - Bedrock + WordPress via Composer. # --------------------------------------------------------------------------- @@ -170,4 +187,6 @@ volumes: php-socket: uploads: cache: + caddy-data: + caddy-config: diff --git a/docker/caddy/Caddyfile b/docker/caddy/Caddyfile new file mode 100644 index 0000000..d919088 --- /dev/null +++ b/docker/caddy/Caddyfile @@ -0,0 +1,18 @@ +{ + # Local development only. + # Caddy will generate a local (untrusted) certificate via its internal CA. + # For a trusted cert, use mkcert and configure Caddy with your cert files. + auto_https disable_redirects +} + +# HTTPS dev endpoint: +# https://wp.localhost:8443 +wp.localhost:8443 { + tls internal + + encode zstd gzip + + # Reverse proxy to Nginx (which then fastcgi_pass to php-fpm). + reverse_proxy web:8080 +} + diff --git a/docker/nginx/conf.d/default.conf b/docker/nginx/conf.d/default.conf index 85a03bf..c1b7f72 100644 --- a/docker/nginx/conf.d/default.conf +++ b/docker/nginx/conf.d/default.conf @@ -60,6 +60,10 @@ server { try_files $uri =404; include fastcgi_params; fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; + fastcgi_param HTTP_X_FORWARDED_HOST $http_x_forwarded_host; + fastcgi_param HTTP_X_FORWARDED_PORT $http_x_forwarded_port; + fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_index index.php; @@ -72,6 +76,7 @@ server { access_log off; include fastcgi_params; fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; fastcgi_param SCRIPT_FILENAME $document_root/index.php; fastcgi_pass unix:/var/run/php/php-fpm.sock; } @@ -80,6 +85,7 @@ server { access_log off; include fastcgi_params; fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; fastcgi_param SCRIPT_FILENAME $document_root/index.php; fastcgi_pass unix:/var/run/php/php-fpm.sock; allow 127.0.0.1; From 7aa19b29faf8ca255b4ece562a14e4da43736d37 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 12:15:28 +0000 Subject: [PATCH 19/29] feat: add optional php-fpm metrics exporter Co-authored-by: Manuel H. --- compose.yaml | 15 ++++++++++ helm/wp-boilerplate/README.md | 15 ++++++++++ helm/wp-boilerplate/templates/deployment.yaml | 28 +++++++++++++++++++ helm/wp-boilerplate/templates/service.yaml | 5 ++++ .../templates/servicemonitor.yaml | 21 ++++++++++++++ helm/wp-boilerplate/values.yaml | 14 ++++++++++ 6 files changed, 98 insertions(+) create mode 100644 helm/wp-boilerplate/templates/servicemonitor.yaml diff --git a/compose.yaml b/compose.yaml index 2e63db7..e14f684 100644 --- a/compose.yaml +++ b/compose.yaml @@ -181,6 +181,21 @@ services: web: condition: service_healthy + php-fpm-exporter: + image: hipages/php-fpm_exporter:v2.2.0 + profiles: ["observability"] + user: "101:101" + environment: + PHP_FPM_SCRAPE_URI: unix:///var/run/php/php-fpm.sock;/status + PHP_FPM_FIX_PROCESS_COUNT: "true" + volumes: + - php-socket:/var/run/php:ro + ports: + - "9253:9253" + depends_on: + php: + condition: service_started + volumes: db-data: redis-data: diff --git a/helm/wp-boilerplate/README.md b/helm/wp-boilerplate/README.md index 5a6f1ee..5bfdb09 100644 --- a/helm/wp-boilerplate/README.md +++ b/helm/wp-boilerplate/README.md @@ -83,3 +83,18 @@ cron: schedule: "*/5 * * * *" ``` +## Metrics (optional) + +This chart can run a `php-fpm_exporter` sidecar (Prometheus) and optionally create a `ServiceMonitor`. + +Example: + +```yaml +metrics: + enabled: true + serviceMonitor: + enabled: true + labels: + release: prometheus +``` + diff --git a/helm/wp-boilerplate/templates/deployment.yaml b/helm/wp-boilerplate/templates/deployment.yaml index dceb36e..8942eac 100644 --- a/helm/wp-boilerplate/templates/deployment.yaml +++ b/helm/wp-boilerplate/templates/deployment.yaml @@ -108,3 +108,31 @@ spec: resources: {{- toYaml .Values.resources.php | nindent 12 }} + {{- if .Values.metrics.enabled }} + - name: php-fpm-exporter + image: "{{ .Values.metrics.phpFpmExporter.image }}:{{ .Values.metrics.phpFpmExporter.tag }}" + imagePullPolicy: {{ .Values.metrics.phpFpmExporter.pullPolicy }} + ports: + - name: metrics + containerPort: {{ .Values.metrics.phpFpmExporter.port }} + env: + - name: PHP_FPM_SCRAPE_URI + value: "unix:///var/run/php/php-fpm.sock;/status" + - name: PHP_FPM_FIX_PROCESS_COUNT + value: "true" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + capabilities: + drop: ["ALL"] + volumeMounts: + - name: php-socket + mountPath: /var/run/php + readOnly: true + resources: + {{- toYaml .Values.metrics.phpFpmExporter.resources | nindent 12 }} + {{- end }} + diff --git a/helm/wp-boilerplate/templates/service.yaml b/helm/wp-boilerplate/templates/service.yaml index 1eff073..78f01da 100644 --- a/helm/wp-boilerplate/templates/service.yaml +++ b/helm/wp-boilerplate/templates/service.yaml @@ -12,4 +12,9 @@ spec: - name: http port: {{ .Values.service.port }} targetPort: http + {{- if .Values.metrics.enabled }} + - name: metrics + port: {{ .Values.metrics.phpFpmExporter.port }} + targetPort: metrics + {{- end }} diff --git a/helm/wp-boilerplate/templates/servicemonitor.yaml b/helm/wp-boilerplate/templates/servicemonitor.yaml new file mode 100644 index 0000000..c6a56a6 --- /dev/null +++ b/helm/wp-boilerplate/templates/servicemonitor.yaml @@ -0,0 +1,21 @@ +{{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled -}} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "wp-boilerplate.fullname" . }} + labels: + {{- include "wp-boilerplate.labels" . | nindent 4 }} + {{- with .Values.metrics.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "wp-boilerplate.selectorLabels" . | nindent 6 }} + endpoints: + - port: metrics + path: /metrics + interval: {{ .Values.metrics.serviceMonitor.interval }} + scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }} +{{- end }} + diff --git a/helm/wp-boilerplate/values.yaml b/helm/wp-boilerplate/values.yaml index 047366d..b6c4269 100644 --- a/helm/wp-boilerplate/values.yaml +++ b/helm/wp-boilerplate/values.yaml @@ -62,6 +62,20 @@ resources: php: {} web: {} +metrics: + enabled: false + phpFpmExporter: + image: hipages/php-fpm_exporter + tag: v2.2.0 + pullPolicy: IfNotPresent + port: 9253 + resources: {} + serviceMonitor: + enabled: false + interval: 30s + scrapeTimeout: 10s + labels: {} + autoscaling: enabled: false minReplicas: 1 From 41bb3ef13654ce0644725c790bde3ad82c03f6e8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 12:16:24 +0000 Subject: [PATCH 20/29] chore(ci): validate Helm renders with kubeconform Co-authored-by: Manuel H. --- .github/workflows/ci.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b72ede..08a294b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,3 +61,21 @@ jobs: --set image.web.tag=ci > /tmp/wp-boilerplate.rendered.yaml + - name: Install kubeconform + run: | + set -euo pipefail + KUBECONFORM_VERSION="v0.7.0" + curl -fsSLo kubeconform.tar.gz "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" + tar -xzf kubeconform.tar.gz kubeconform + sudo mv kubeconform /usr/local/bin/kubeconform + + - name: Kubeconform validate rendered manifests + run: | + set -euo pipefail + kubeconform \ + -strict \ + -summary \ + -ignore-missing-schemas \ + -kubernetes-version 1.29.0 \ + /tmp/wp-boilerplate.rendered.yaml + From acc06754c6e2052473c2f2a03ffc9923f84bbf75 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 12:18:16 +0000 Subject: [PATCH 21/29] docs: add cosign verification instructions Co-authored-by: Manuel H. --- README.md | 4 +++ docs/supply-chain/README.md | 12 +++++++++ docs/supply-chain/cosign.md | 51 +++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 docs/supply-chain/README.md create mode 100644 docs/supply-chain/cosign.md diff --git a/README.md b/README.md index ac8708f..7c00fa5 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,10 @@ Recommended approaches: See `docs/kubernetes/` for templates and required keys. +## Supply chain / image verification + +CI signs published images (keyless Cosign). See `docs/supply-chain/cosign.md`. + --- ## License diff --git a/docs/supply-chain/README.md b/docs/supply-chain/README.md new file mode 100644 index 0000000..0d22235 --- /dev/null +++ b/docs/supply-chain/README.md @@ -0,0 +1,12 @@ +# Supply Chain & Image Verification + +This repository aims to be **build-once / deploy-everywhere**: + +- CI builds container images and publishes to GHCR +- Deployments should pin images **by digest** +- CI **signs** published images using **Cosign (keyless, GitHub OIDC)** + +See: + +- `docs/supply-chain/cosign.md` + diff --git a/docs/supply-chain/cosign.md b/docs/supply-chain/cosign.md new file mode 100644 index 0000000..153d768 --- /dev/null +++ b/docs/supply-chain/cosign.md @@ -0,0 +1,51 @@ +# Cosign (Keyless) Verification + +The container build workflow signs published images using **Cosign** with **GitHub OIDC** (no long-lived signing keys in the repo). + +Workflow: + +- `.github/workflows/container-build.yml` + +## Verify a published image + +Install cosign: + +- https://docs.sigstore.dev/cosign/system_config/installation/ + +Then verify an image digest: + +```bash +cosign verify \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity "https://github.com///.github/workflows/container-build.yml@refs/heads/" \ + ghcr.io//-php@sha256: +``` + +Repeat for the nginx image: + +```bash +cosign verify \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity "https://github.com///.github/workflows/container-build.yml@refs/heads/" \ + ghcr.io//-nginx@sha256: +``` + +Notes: + +- Images are only published/signed on non-PR events in the workflow (PRs build but do not push). +- Prefer verifying **by digest**, not by tag. + +## SBOM / provenance (optional) + +The workflow also publishes SBOM/provenance attestations via BuildKit. + +Depending on your cosign version and registry support, you can fetch them: + +```bash +cosign download sbom ghcr.io//-php@sha256: +``` + +```bash +cosign download attestation ghcr.io//-php@sha256: +``` + From 21118b471831ca221aac0bface5a833dc1276324 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 16:23:26 +0000 Subject: [PATCH 22/29] feat: add trusted local TLS flow with mkcert Co-authored-by: Manuel H. --- .gitignore | 3 ++ Makefile | 20 ++++++++++++- README.md | 19 ++++++++++-- compose.yaml | 18 ++++++++++++ docker/caddy/Caddyfile.mkcert | 20 +++++++++++++ docs/local-dev/tls-mkcert.md | 54 +++++++++++++++++++++++++++++++++++ 6 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 docker/caddy/Caddyfile.mkcert create mode 100644 docs/local-dev/tls-mkcert.md diff --git a/.gitignore b/.gitignore index 04885f1..47b1cd2 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ node_modules .idea .vscode +# Local TLS certs (mkcert) +.certs/ + diff --git a/Makefile b/Makefile index 7c342fd..af8a7c5 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ COMPOSE ?= docker compose RUN_USER ?= $(shell id -u 2>/dev/null || echo 1000):$(shell id -g 2>/dev/null || echo 1000) .PHONY: help install composer-install up down restart ps logs shell up-mail up-dbadmin up-observability wp composer -.PHONY: bootstrap env wait wp-install up-tls bootstrap-tls +.PHONY: bootstrap env wait wp-install up-tls bootstrap-tls certs-mkcert up-tls-trusted bootstrap-tls-trusted help: ## Show available targets @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_.-]+:.*##/ {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -49,6 +49,24 @@ bootstrap-tls: ## Bootstrap stack + local TLS proxy (requires WP_HOME/WP_SITEURL $(MAKE) wait $(MAKE) wp-install +certs-mkcert: ## Generate trusted local certs for wp.localhost using mkcert + @command -v mkcert >/dev/null 2>&1 || { echo "mkcert not found. Install it first: https://github.com/FiloSottile/mkcert"; exit 1; } + @mkdir -p .certs + @mkcert -install + @mkcert -cert-file .certs/wp.localhost.pem -key-file .certs/wp.localhost-key.pem wp.localhost + @echo "Generated .certs/wp.localhost.pem and .certs/wp.localhost-key.pem" + +up-tls-trusted: ## Start dev stack + trusted local TLS proxy (mkcert) + $(COMPOSE) --profile tls-trusted up -d --build + +bootstrap-tls-trusted: ## Bootstrap stack + trusted local TLS (runs mkcert first) + $(MAKE) env + $(MAKE) certs-mkcert + $(MAKE) composer-install + $(MAKE) up-tls-trusted + $(MAKE) wait + $(MAKE) wp-install + down: ## Stop dev stack $(COMPOSE) down --remove-orphans diff --git a/README.md b/README.md index 7c00fa5..253ccba 100644 --- a/README.md +++ b/README.md @@ -51,18 +51,33 @@ make down ### Optional: local HTTPS (TLS) -This repo includes an optional Caddy reverse proxy for local HTTPS: +This repo includes optional Caddy reverse-proxy profiles for local HTTPS: + +**Option A: quick internal CA (browser warns)** ```bash # Update .env so WP_HOME/WP_SITEURL use https://wp.localhost:8443 make bootstrap-tls ``` +**Option B: trusted certs with mkcert (recommended for local UX)** + +```bash +# Requires mkcert installed on your machine +make bootstrap-tls-trusted +``` + URL: - https://wp.localhost:8443 -Note: Caddy uses a locally-generated certificate (browser will warn). For a trusted cert, use `mkcert` and configure Caddy with your generated certs. +Notes: + +- For trusted TLS, `make certs-mkcert` generates local cert files under `.certs/` (gitignored). +- Set in `.env`: + - `WP_HOME=https://wp.localhost:8443` + - `WP_SITEURL=https://wp.localhost:8443/wp` +- Detailed guide: `docs/local-dev/tls-mkcert.md` ### Optional profiles (dev conveniences) diff --git a/compose.yaml b/compose.yaml index e14f684..169f58a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -39,6 +39,24 @@ services: web: condition: service_healthy + # --------------------------------------------------------------------------- + # Optional: TLS reverse proxy with mkcert-provided trusted certs. + # URL: https://wp.localhost:8443 + # --------------------------------------------------------------------------- + caddy-mkcert: + image: caddy:2-alpine + profiles: ["tls-trusted"] + ports: + - "8443:8443" + volumes: + - ./docker/caddy/Caddyfile.mkcert:/etc/caddy/Caddyfile:ro + - ./.certs:/certs:ro + - caddy-data:/data + - caddy-config:/config + depends_on: + web: + condition: service_healthy + # --------------------------------------------------------------------------- # PHP-FPM - Bedrock + WordPress via Composer. # --------------------------------------------------------------------------- diff --git a/docker/caddy/Caddyfile.mkcert b/docker/caddy/Caddyfile.mkcert new file mode 100644 index 0000000..9b903ac --- /dev/null +++ b/docker/caddy/Caddyfile.mkcert @@ -0,0 +1,20 @@ +{ + # Local development only. + auto_https disable_redirects +} + +# HTTPS dev endpoint with mkcert-provided certificate: +# https://wp.localhost:8443 +# +# Expected files (generated by `make certs-mkcert`): +# .certs/wp.localhost.pem +# .certs/wp.localhost-key.pem +wp.localhost:8443 { + tls /certs/wp.localhost.pem /certs/wp.localhost-key.pem + + encode zstd gzip + + # Reverse proxy to Nginx (which then fastcgi_pass to php-fpm). + reverse_proxy web:8080 +} + diff --git a/docs/local-dev/tls-mkcert.md b/docs/local-dev/tls-mkcert.md new file mode 100644 index 0000000..8432836 --- /dev/null +++ b/docs/local-dev/tls-mkcert.md @@ -0,0 +1,54 @@ +# Trusted Local TLS with mkcert + +This repo supports trusted local HTTPS with: + +- `mkcert` for certificate generation +- Caddy profile `tls-trusted` for reverse proxy + +## Why + +The default TLS profile uses Caddy internal certs (quick setup, browser warning). +For day-to-day local dev (cookies, OAuth callbacks, browser APIs), trusted certs are better. + +## Prerequisites + +- `mkcert` installed + - macOS (Homebrew): `brew install mkcert nss` + - Linux: see https://github.com/FiloSottile/mkcert#linux + - Windows (Chocolatey): `choco install mkcert` + +## Steps + +1. Configure `.env`: + +```dotenv +WP_HOME=https://wp.localhost:8443 +WP_SITEURL=https://wp.localhost:8443/wp +``` + +2. Generate trusted certs: + +```bash +make certs-mkcert +``` + +This creates (gitignored): + +- `.certs/wp.localhost.pem` +- `.certs/wp.localhost-key.pem` + +3. Start stack: + +```bash +make bootstrap-tls-trusted +``` + +4. Open: + +- https://wp.localhost:8443 + +## Troubleshooting + +- If you still see cert warnings, re-run `mkcert -install` and restart your browser. +- Ensure your OS trust store accepted mkcert's local CA. + From cadcfbebf8d6452d4f529f6b2c804135795091ec Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 19:39:29 +0000 Subject: [PATCH 23/29] feat: add local doctor preflight checks Co-authored-by: Manuel H. --- Makefile | 5 +- README.md | 1 + scripts/doctor.sh | 114 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100755 scripts/doctor.sh diff --git a/Makefile b/Makefile index af8a7c5..bfb8a15 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ COMPOSE ?= docker compose RUN_USER ?= $(shell id -u 2>/dev/null || echo 1000):$(shell id -g 2>/dev/null || echo 1000) .PHONY: help install composer-install up down restart ps logs shell up-mail up-dbadmin up-observability wp composer -.PHONY: bootstrap env wait wp-install up-tls bootstrap-tls certs-mkcert up-tls-trusted bootstrap-tls-trusted +.PHONY: bootstrap env wait wp-install up-tls bootstrap-tls certs-mkcert up-tls-trusted bootstrap-tls-trusted doctor help: ## Show available targets @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_.-]+:.*##/ {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -12,6 +12,9 @@ help: ## Show available targets env: ## Create .env from .env.example if missing @if [ -f .env ]; then echo ".env exists"; else cp .env.example .env && echo "Created .env from .env.example"; fi +doctor: ## Preflight checks (docker, compose, ports, env, optional mkcert) + @bash scripts/doctor.sh + up: ## Start dev stack (build + up) $(COMPOSE) up -d --build diff --git a/README.md b/README.md index 253ccba..963e071 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ A production-minded WordPress boilerplate based on **[Roots Bedrock](https://roo ```bash cp .env.example .env +make doctor make bootstrap ``` diff --git a/scripts/doctor.sh b/scripts/doctor.sh new file mode 100755 index 0000000..c39fec7 --- /dev/null +++ b/scripts/doctor.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +set -euo pipefail + +errors=() +warnings=() + +add_error() { + errors+=("$1") +} + +add_warning() { + warnings+=("$1") +} + +require_cmd() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + add_error "Missing required command: $cmd" + fi +} + +check_port_free() { + local port="$1" + local label="$2" + + if ! command -v ss >/dev/null 2>&1; then + add_warning "Cannot check port $port ($label): 'ss' command not found." + return + fi + + if ss -ltnH "sport = :$port" | rg -q .; then + add_error "Port $port is already in use ($label)." + fi +} + +print_section() { + local title="$1" + printf "\n== %s ==\n" "$title" +} + +print_section "WordPress Boilerplate Doctor" + +require_cmd docker +require_cmd make +require_cmd rg + +if command -v docker >/dev/null 2>&1; then + if ! docker info >/dev/null 2>&1; then + add_error "Docker daemon is not reachable (is Docker running?)." + fi + + if ! docker compose version >/dev/null 2>&1; then + add_error "Docker Compose v2 plugin is not available (docker compose)." + fi +fi + +if ! command -v mkcert >/dev/null 2>&1; then + add_warning "mkcert not found (only needed for trusted local TLS profile)." +fi + +if [[ ! -f ".env" ]]; then + add_warning ".env is missing. Run: cp .env.example .env" +else + wp_home="$(awk -F= '/^WP_HOME=/{print $2; exit}' .env | tr -d '\r' || true)" + wp_siteurl="$(awk -F= '/^WP_SITEURL=/{print $2; exit}' .env | tr -d '\r' || true)" + + if [[ -z "$wp_home" ]]; then + add_warning "WP_HOME is not set in .env." + fi + + if [[ -z "$wp_siteurl" ]]; then + add_warning "WP_SITEURL is not set in .env." + fi + + if [[ "$wp_home" == "https://wp.localhost:8443" ]]; then + if [[ ! -f ".certs/wp.localhost.pem" || ! -f ".certs/wp.localhost-key.pem" ]]; then + add_warning "Trusted TLS is configured but cert files are missing. Run: make certs-mkcert" + fi + fi +fi + +check_port_free 8080 "local HTTP (web)" +check_port_free 8443 "local HTTPS (caddy)" +check_port_free 8081 "phpMyAdmin profile" +check_port_free 8025 "MailHog profile" + +print_section "Summary" + +if ((${#errors[@]} > 0)); then + printf "Errors:\n" + for message in "${errors[@]}"; do + printf " - %s\n" "$message" + done +fi + +if ((${#warnings[@]} > 0)); then + printf "Warnings:\n" + for message in "${warnings[@]}"; do + printf " - %s\n" "$message" + done +fi + +if ((${#errors[@]} == 0)); then + printf "Doctor checks passed.\n" + if ((${#warnings[@]} > 0)); then + printf "Proceed with caution and review warnings above.\n" + fi + exit 0 +fi + +printf "Doctor checks failed. Fix errors before running bootstrap.\n" +exit 1 + From 011acd108d5f58b49570a6741b50222e675a85cd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 19:39:43 +0000 Subject: [PATCH 24/29] chore(ci): add docker compose local smoke test Co-authored-by: Manuel H. --- .github/workflows/ci.yml | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08a294b..774fec3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,49 @@ jobs: - name: Composer audit run: composer audit + local-smoke: + name: Local stack smoke (Docker Compose) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Prepare local .env for CI + run: | + cp .env.example .env + echo "APP_UID=$(id -u)" >> .env + echo "APP_GID=$(id -g)" >> .env + echo "WP_ADMIN_PASSWORD=ci-smoke-password" >> .env + + - name: Docker versions + run: | + docker version + docker compose version + + - name: Doctor preflight + run: make doctor + + - name: Bootstrap stack + run: make bootstrap + + - name: Smoke checks + run: | + set -euo pipefail + curl -fsS http://localhost:8080/ping >/dev/null + curl -fsS http://localhost:8080/wp/wp-login.php | rg -qi "user_login|wordpress" + make wp ARGS="core is-installed --path=web/wp" + + - name: Compose status/logs on failure + if: failure() + run: | + docker compose ps + docker compose logs --no-color --tail=200 + + - name: Teardown + if: always() + run: docker compose down -v --remove-orphans + helm: name: Helm lint / template runs-on: ubuntu-latest From 567a44ecd6ccbaa4dfaa8cc256dd3e62cf7c6eed Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 19:47:06 +0000 Subject: [PATCH 25/29] feat: add reusable local smoke test target Co-authored-by: Manuel H. --- Makefile | 10 ++++++++- README.md | 1 + scripts/smoke.sh | 58 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100755 scripts/smoke.sh diff --git a/Makefile b/Makefile index bfb8a15..f89760e 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ COMPOSE ?= docker compose RUN_USER ?= $(shell id -u 2>/dev/null || echo 1000):$(shell id -g 2>/dev/null || echo 1000) .PHONY: help install composer-install up down restart ps logs shell up-mail up-dbadmin up-observability wp composer -.PHONY: bootstrap env wait wp-install up-tls bootstrap-tls certs-mkcert up-tls-trusted bootstrap-tls-trusted doctor +.PHONY: bootstrap env wait wp-install up-tls bootstrap-tls certs-mkcert up-tls-trusted bootstrap-tls-trusted doctor smoke smoke-full help: ## Show available targets @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_.-]+:.*##/ {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -15,6 +15,14 @@ env: ## Create .env from .env.example if missing doctor: ## Preflight checks (docker, compose, ports, env, optional mkcert) @bash scripts/doctor.sh +smoke: ## Smoke-test running local stack (HTTP + wp core is-installed) + @bash scripts/smoke.sh + +smoke-full: ## Doctor + bootstrap + smoke checks + $(MAKE) doctor + $(MAKE) bootstrap + $(MAKE) smoke + up: ## Start dev stack (build + up) $(COMPOSE) up -d --build diff --git a/README.md b/README.md index 963e071..c6a736d 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ A production-minded WordPress boilerplate based on **[Roots Bedrock](https://roo cp .env.example .env make doctor make bootstrap +make smoke ``` Then open: diff --git a/scripts/smoke.sh b/scripts/smoke.sh new file mode 100755 index 0000000..c40505f --- /dev/null +++ b/scripts/smoke.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if ! command -v docker >/dev/null 2>&1; then + echo "docker is required for smoke checks." + exit 1 +fi + +if ! command -v rg >/dev/null 2>&1; then + echo "rg is required for smoke checks." + exit 1 +fi + +if ! docker compose ps --services --filter status=running | rg -q '^web$'; then + echo "web service is not running. Start the stack first (for example: make bootstrap)." + exit 1 +fi + +fetch_url() { + local url="$1" + if command -v curl >/dev/null 2>&1; then + curl -fsS "$url" + return + fi + + if command -v wget >/dev/null 2>&1; then + wget -qO- "$url" + return + fi + + echo "Neither curl nor wget is available." + return 1 +} + +echo "Checking /ping endpoint ..." +for _ in $(seq 1 20); do + if fetch_url "http://localhost:8080/ping" >/dev/null 2>&1; then + break + fi + sleep 1 +done + +fetch_url "http://localhost:8080/ping" >/dev/null + +echo "Checking WordPress login page ..." +login_page="$(fetch_url "http://localhost:8080/wp/wp-login.php")" +if ! printf '%s' "$login_page" | rg -qi "user_login|wordpress"; then + echo "Unexpected response from wp-login page." + exit 1 +fi + +echo "Checking WP installation state ..." +run_user="$(id -u 2>/dev/null || echo 1000):$(id -g 2>/dev/null || echo 1000)" +docker compose --profile tools run --rm --user "$run_user" wp --path=web/wp core is-installed >/dev/null + +echo "Smoke checks passed." + From fe2fcdbbc1b49944050071af7ab6c209cc783e61 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Feb 2026 19:47:21 +0000 Subject: [PATCH 26/29] chore(ci): use make smoke in local smoke job Co-authored-by: Manuel H. --- .github/workflows/ci.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 774fec3..a0bc203 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,11 +64,7 @@ jobs: run: make bootstrap - name: Smoke checks - run: | - set -euo pipefail - curl -fsS http://localhost:8080/ping >/dev/null - curl -fsS http://localhost:8080/wp/wp-login.php | rg -qi "user_login|wordpress" - make wp ARGS="core is-installed --path=web/wp" + run: make smoke - name: Compose status/logs on failure if: failure() From e3e855deed463fb560fd237137f450ac1378782a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 00:45:36 +0000 Subject: [PATCH 27/29] feat: add unified make qa workflow Co-authored-by: Manuel H. --- CONTRIBUTING.md | 6 ++++++ Makefile | 5 ++++- README.md | 2 ++ scripts/qa.sh | 27 +++++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100755 scripts/qa.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4dc42c7..abe6c76 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,3 +21,9 @@ The CI builds branch-scoped container images and (on `main`) produces stable ima See the repository README for Docker Compose commands. +Before opening a PR, run: + +```bash +make qa +``` + diff --git a/Makefile b/Makefile index f89760e..b5edd1e 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ COMPOSE ?= docker compose RUN_USER ?= $(shell id -u 2>/dev/null || echo 1000):$(shell id -g 2>/dev/null || echo 1000) .PHONY: help install composer-install up down restart ps logs shell up-mail up-dbadmin up-observability wp composer -.PHONY: bootstrap env wait wp-install up-tls bootstrap-tls certs-mkcert up-tls-trusted bootstrap-tls-trusted doctor smoke smoke-full +.PHONY: bootstrap env wait wp-install up-tls bootstrap-tls certs-mkcert up-tls-trusted bootstrap-tls-trusted doctor smoke smoke-full qa help: ## Show available targets @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_.-]+:.*##/ {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -23,6 +23,9 @@ smoke-full: ## Doctor + bootstrap + smoke checks $(MAKE) bootstrap $(MAKE) smoke +qa: ## Full QA: composer checks + docker smoke-full when available + @bash scripts/qa.sh + up: ## Start dev stack (build + up) $(COMPOSE) up -d --build diff --git a/README.md b/README.md index c6a736d..da54fbb 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ cp .env.example .env make doctor make bootstrap make smoke +# Full pre-push QA bundle (runs composer checks; runs smoke-full when Docker is available) +make qa ``` Then open: diff --git a/scripts/qa.sh b/scripts/qa.sh new file mode 100755 index 0000000..cda55a0 --- /dev/null +++ b/scripts/qa.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -euo pipefail + +echo "== QA: Composer checks ==" +composer validate --strict +composer lint +composer audit + +docker_available=false +if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then + docker_available=true +fi + +if [[ "$docker_available" == "false" ]]; then + echo "== QA: Docker checks skipped ==" + echo "Docker CLI/daemon is not available. Skipping doctor/smoke-full." + if [[ "${QA_DOCKER_REQUIRED:-0}" == "1" ]]; then + echo "QA_DOCKER_REQUIRED=1 is set; failing because Docker is unavailable." + exit 1 + fi + exit 0 +fi + +echo "== QA: Docker checks ==" +make smoke-full + From 371c5624dcb1c95d1a003dea70faf96c8714b4ac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 16:06:48 +0000 Subject: [PATCH 28/29] fix: address review feedback for portability and defaults Co-authored-by: Manuel H. --- Dockerfile | 2 +- config/application.php | 4 ++-- helm/wp-boilerplate/README.md | 6 ++++++ helm/wp-boilerplate/templates/cronjob.yaml | 2 +- helm/wp-boilerplate/values.yaml | 3 +++ scripts/doctor.sh | 16 ++++++++++------ scripts/smoke.sh | 9 ++------- 7 files changed, 25 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index e25dc0c..c75306c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,7 @@ RUN set -eux; \ gd \ ; \ pecl install redis apcu; \ - docker-php-ext-enable redis apcu opcache; \ + docker-php-ext-enable redis apcu; \ apk del .build-deps # ----------------------------------------------------------------------------- diff --git a/config/application.php b/config/application.php index c89c0e4..6ecd565 100644 --- a/config/application.php +++ b/config/application.php @@ -166,8 +166,8 @@ // Override in development/staging config if needed. Config::define('DISALLOW_FILE_MODS', env('DISALLOW_FILE_MODS') ?? true); -// Limit the number of post revisions. -Config::define('WP_POST_REVISIONS', env('WP_POST_REVISIONS') ?? true); +// Limit the number of post revisions (avoid unbounded revision growth by default). +Config::define('WP_POST_REVISIONS', env('WP_POST_REVISIONS') ?? 25); // Disable script concatenation (avoid surprises behind CDNs/proxies). Config::define('CONCATENATE_SCRIPTS', false); diff --git a/helm/wp-boilerplate/README.md b/helm/wp-boilerplate/README.md index 5bfdb09..62305ff 100644 --- a/helm/wp-boilerplate/README.md +++ b/helm/wp-boilerplate/README.md @@ -83,6 +83,12 @@ cron: schedule: "*/5 * * * *" ``` +If your cluster uses a non-default DNS domain, set: + +```yaml +clusterDomain: cluster.local +``` + ## Metrics (optional) This chart can run a `php-fpm_exporter` sidecar (Prometheus) and optionally create a `ServiceMonitor`. diff --git a/helm/wp-boilerplate/templates/cronjob.yaml b/helm/wp-boilerplate/templates/cronjob.yaml index 9ece66e..d349496 100644 --- a/helm/wp-boilerplate/templates/cronjob.yaml +++ b/helm/wp-boilerplate/templates/cronjob.yaml @@ -36,7 +36,7 @@ spec: - -c - > curl -fsSL - "http://{{ include "wp-boilerplate.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }}{{ .Values.cron.urlPath }}" + "http://{{ include "wp-boilerplate.fullname" . }}.{{ .Release.Namespace }}.svc.{{ .Values.clusterDomain }}:{{ .Values.service.port }}{{ .Values.cron.urlPath }}" >/dev/null {{- end }} diff --git a/helm/wp-boilerplate/values.yaml b/helm/wp-boilerplate/values.yaml index b6c4269..a561327 100644 --- a/helm/wp-boilerplate/values.yaml +++ b/helm/wp-boilerplate/values.yaml @@ -47,6 +47,9 @@ service: port: 80 targetPort: 8080 +# Kubernetes cluster DNS domain. +clusterDomain: cluster.local + ingress: enabled: false className: "" diff --git a/scripts/doctor.sh b/scripts/doctor.sh index c39fec7..17d5486 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -23,14 +23,19 @@ require_cmd() { check_port_free() { local port="$1" local label="$2" + local severity="${3:-error}" if ! command -v ss >/dev/null 2>&1; then add_warning "Cannot check port $port ($label): 'ss' command not found." return fi - if ss -ltnH "sport = :$port" | rg -q .; then - add_error "Port $port is already in use ($label)." + if ss -ltnH "sport = :$port" | grep -q .; then + if [[ "$severity" == "warning" ]]; then + add_warning "Port $port is already in use ($label)." + else + add_error "Port $port is already in use ($label)." + fi fi } @@ -43,7 +48,6 @@ print_section "WordPress Boilerplate Doctor" require_cmd docker require_cmd make -require_cmd rg if command -v docker >/dev/null 2>&1; then if ! docker info >/dev/null 2>&1; then @@ -81,9 +85,9 @@ else fi check_port_free 8080 "local HTTP (web)" -check_port_free 8443 "local HTTPS (caddy)" -check_port_free 8081 "phpMyAdmin profile" -check_port_free 8025 "MailHog profile" +check_port_free 8443 "local HTTPS (caddy profile)" warning +check_port_free 8081 "phpMyAdmin profile" warning +check_port_free 8025 "MailHog profile" warning print_section "Summary" diff --git a/scripts/smoke.sh b/scripts/smoke.sh index c40505f..1101acf 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -7,12 +7,7 @@ if ! command -v docker >/dev/null 2>&1; then exit 1 fi -if ! command -v rg >/dev/null 2>&1; then - echo "rg is required for smoke checks." - exit 1 -fi - -if ! docker compose ps --services --filter status=running | rg -q '^web$'; then +if ! docker compose ps --services --filter status=running | grep -q '^web$'; then echo "web service is not running. Start the stack first (for example: make bootstrap)." exit 1 fi @@ -45,7 +40,7 @@ fetch_url "http://localhost:8080/ping" >/dev/null echo "Checking WordPress login page ..." login_page="$(fetch_url "http://localhost:8080/wp/wp-login.php")" -if ! printf '%s' "$login_page" | rg -qi "user_login|wordpress"; then +if ! printf '%s' "$login_page" | grep -Eqi "user_login|wordpress"; then echo "Unexpected response from wp-login page." exit 1 fi From f3f9b6f63e84f8a57e84a551bd120d1fc30866de Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Feb 2026 16:09:13 +0000 Subject: [PATCH 29/29] chore: add SonarCloud configuration file Co-authored-by: Manuel H. --- .sonarcloud.properties | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .sonarcloud.properties diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 0000000..1cc31ad --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,18 @@ +# SonarCloud configuration for this boilerplate repository. +# +# Project identity is typically provided by CI (recommended), for example: +# -Dsonar.organization= +# -Dsonar.projectKey=_ +# +# You can uncomment and set the values below if you prefer storing them here. +# sonar.organization= +# sonar.projectKey= + +sonar.projectName=Modern Cloud Native WordPress Boilerplate +sonar.sourceEncoding=UTF-8 + +# Scan the repository, then explicitly exclude generated/third-party/runtime paths. +sonar.sources=. +sonar.exclusions=vendor/**,web/wp/**,web/app/uploads/**,web/app/plugins/**,web/app/cache/**,.certs/**,**/.gitkeep +sonar.cpd.exclusions=vendor/**,web/wp/** +