A modern WordPress project built on Roots Bedrock and the Sage starter theme, containerised for Google Cloud Platform with a focus on minimal infrastructure cost.
Stack
- WordPress 6.9 managed by Composer (Bedrock)
- Sage theme with Acorn (Laravel in WordPress), Blade templating, Tailwind CSS v4, Vite 8
- PHP 8.4 + nginx + supervisord (single container)
- MariaDB 11
- GCP Cloud Run + Cloud Build (Kaniko) + Artifact Registry
| Tool | Version |
|---|---|
| Docker Engine + Compose v2 | latest |
| Node.js | ^20.19.0 or >=22.12.0 |
| PHP | 8.4 (must match Dockerfile — lock file is pinned to PHP 8.4) |
| Composer | ^2.9 |
PHP and Composer are only needed on the host for IDE tooling (PHPStan, Pint). The application itself runs entirely in Docker.
- GCP project with billing enabled
- APIs enabled: Cloud Build, Cloud Run, Artifact Registry
- Cloud SQL instance (MySQL 8 or compatible) or a managed MySQL host
- Cloud Build service account with:
roles/artifactregistry.writerroles/storage.admin(Kaniko layer cache)roles/run.admin+roles/iam.serviceAccountUser(if using the Cloud Run deploy step)
git clone <repo-url> jussipress-ai
cd jussipress-ai
cp .env.example .envEdit .env — the minimum required values:
DB_NAME=wordpress
DB_USER=wordpress
DB_PASSWORD=wordpress
DB_HOST=db # matches the docker-compose service name
WP_ENV=development
WP_HOME=http://localhost:8080
WP_SITEURL="${WP_HOME}/wp"
# Generate at https://roots.io/salts.html
AUTH_KEY='...'
SECURE_AUTH_KEY='...'
# ... (all 8 keys)# PHP — root project
composer install
# PHP — theme
cd web/app/themes/jussipress-theme && composer install && cd -
# Node — theme
cd web/app/themes/jussipress-theme && npm ci && cd -./dev upThis starts:
app— PHP 8.4-FPM + nginx onhttp://localhost:8080db— MariaDB 11
./dev wp core install \
--url=http://localhost:8080 \
--title="JussiPress AI" \
--admin_user=admin \
--admin_password=secret \
--admin_email=admin@example.comIn a separate terminal:
./dev theme-devVite runs at http://localhost:5173 and injects assets with hot-module replacement into the WordPress site.
./dev up [svc] Start services (detached)
./dev down Stop and remove containers
./dev stop [svc] Stop without removing
./dev restart [svc] Restart service(s)
./dev rebuild [svc] Rebuild image and restart
./dev logs [svc] Follow logs
./dev ps Container status
./dev shell sh into app container (www-data)
./dev root-shell sh into app container (root)
./dev wp <cmd> WP-CLI e.g. ./dev wp plugin list
./dev composer <cmd> Composer inside container
./dev lint PHP style check (Pint)
./dev lint-fix PHP style auto-fix (Pint)
./dev analyse PHP static analysis (PHPStan)
./dev test Run tests (Pest)
./dev db Open MariaDB CLI
./dev db-dump Dump database to stdout
./dev db-import <file> Import a SQL file
./dev theme-install npm ci in theme directory
./dev theme-dev Vite HMR dev server (host)
./dev theme-build Build theme assets for production
./dev theme-lint ESLint check
./dev theme-lint-fix ESLint auto-fix
./dev theme-format Prettier write (JS + CSS)
./dev theme-format-check Prettier check
./dev blade-format Format all Blade templates
./dev blade-format-check Check Blade formatting
You can also source the script to use the commands as shell functions:
source dev
up
logs app| Tool | Scope | Command |
|---|---|---|
| Laravel Pint | PHP (PER preset) | ./dev lint / ./dev lint-fix |
| PHPStan level 5 + phpstan-wordpress | PHP static analysis | ./dev analyse |
| ESLint 9 flat config | resources/js/** |
./dev theme-lint |
| Prettier | JS + CSS | ./dev theme-format |
| blade-formatter | resources/views/**/*.blade.php |
./dev blade-format |
| Pest | PHP tests | ./dev test |
Install the Google Cloud CLI if you haven't already, then:
# Log in with your Google account
gcloud auth login
# Set the active project
gcloud config set project client-jussimatic
# Authorise Docker to push to Artifact Registry in europe-north1
gcloud auth configure-docker europe-north1-docker.pkg.dev
# Enable the required GCP APIs (one-time per project)
gcloud services enable \
cloudbuild.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com \
secretmanager.googleapis.comgcloud artifacts repositories create jussipress-ai \
--repository-format=docker \
--location=europe-north1 \
--description="JussiPress AI container images"Create a trigger in the GCP Console (or via gcloud) pointing to your repository and using cloudbuild.yaml as the build config. The default substitution variables are:
| Variable | Default | Description |
|---|---|---|
_REGION |
europe-north1 |
GCP region |
_REPO |
jussipress-ai |
Artifact Registry repo name |
_IMAGE |
app |
Image name within the repo |
_SERVICE |
jussipress-ai-production |
Cloud Run service name |
_DEPLOY |
true |
Set to false in trigger to skip deploy |
_CLOUDSQL_INSTANCE |
(set in trigger) | Cloud SQL connection name, e.g. project:region:instance |
_DOMAIN |
(set in trigger) | Cloud Run service domain |
_DB_NAME |
(set in trigger) | Database name |
_DB_USER |
(set in trigger) | Database user |
Manually or via the Cloud Build trigger:
gcloud builds submit \
--config=cloudbuild.yaml \
--substitutions=_REGION=europe-north1Kaniko caches layers in Artifact Registry between builds — subsequent builds are significantly faster.
Every build deploys automatically (_DEPLOY=true). To deploy manually:
gcloud run deploy jussipress-ai \
--image=europe-north1-docker.pkg.dev/client-jussimatic/jussipress-ai/app:latest \
--region=europe-north1 \
--platform=managed \
--allow-unauthenticated \
--min-instances=0 \
--max-instances=3 \
--memory=512Mi \
--cpu=1 \
--port=8080 \
--set-env-vars="WP_ENV=production,DB_HOST=...,DB_NAME=...,DB_USER=...,DB_PASSWORD=..."Set all sensitive values (
DB_PASSWORD, WordPress salts, etc.) as Cloud Run secrets rather than plain environment variables.
Persistent media uploads (web/app/uploads/) must be stored externally since Cloud Run containers are stateless. This project uses WP Stateless to offload all media to the jussipress-bucket GCS bucket. Configure it in Settings → StatelessMedia after first deploy — no JSON key needed, Cloud Run authenticates via the default service account.
Ensure the Cloud Run service account has access to the bucket:
gcloud storage buckets add-iam-policy-binding gs://jussipress-bucket \
--member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
--role="roles/storage.objectAdmin".
├── config/ # WordPress environment config
│ └── environments/ # development.php, staging.php
├── docker/ # nginx, php.ini, supervisord configs
├── web/ # Document root
│ ├── app/
│ │ ├── mu-plugins/ # Must-use plugins
│ │ ├── plugins/ # Composer-managed plugins
│ │ ├── themes/
│ │ │ └── jussipress-theme/ # Sage theme
│ │ │ ├── app/ # PHP — providers, composers
│ │ │ ├── resources/
│ │ │ │ ├── css/ # Tailwind CSS
│ │ │ │ ├── js/ # ES modules
│ │ │ │ └── views/ # Blade templates
│ │ │ └── public/ # Built assets (gitignored)
│ │ └── uploads/ # Media (gitignored, volume-mounted)
│ └── wp/ # WordPress core (gitignored, Composer-managed)
├── Dockerfile # Multi-stage production image
├── docker-compose.yml # Local dev services
├── cloudbuild.yaml # GCP Cloud Build + Kaniko
├── dev # Docker / tooling helper script
└── phpstan.neon # PHPStan configuration