Week 1 infrastructure: Docker environments, deploy pipeline, bookstore-core scaffold

Docker Compose environments for dev/staging/production (MariaDB, Redis,
Caddy, Action Scheduler cron sidecar), an idempotent deploy script,
git-hook-based deploy pipeline, backup/restore scripts, and the
bookstore-core plugin stub with WooCommerce HPOS compatibility declared.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 09:50:18 -04:00
co-authored by Claude Sonnet 5
commit c9d637c907
23 changed files with 757 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
# Copy this to .env.dev / .env.staging / .env.production and fill in real
# values for that environment. The filled-in files are gitignored — never
# commit them. Same code everywhere; only these values differ (see
# design doc §01, Environments & deployment).
# --- Site ---
# (WP_ENVIRONMENT_TYPE is NOT set here — it's hardcoded per environment in
# docker-compose.{dev,staging,production}.yml so it can't go stale.)
SITE_DOMAIN=staging.example.com
SITE_URL=https://staging.example.com
SITE_TITLE="Bookstore (staging)"
# --- WordPress admin bootstrap (used only on first install) ---
WP_ADMIN_USER=admin
WP_ADMIN_PASSWORD=changeme
WP_ADMIN_EMAIL=admin@example.com
# --- Database ---
DB_NAME=bookstore
DB_USER=bookstore
DB_PASSWORD=changeme
DB_ROOT_PASSWORD=changeme
WP_TABLE_PREFIX=wp_
# --- Staging only: basic-auth wall + noindex ---
# Generate the hash with:
# docker run --rm caddy:2-alpine caddy hash-password --plaintext 'your-password'
STAGING_BASIC_AUTH_USER=staging
STAGING_BASIC_AUTH_HASH=
# --- Backups (deploy/backup.sh) ---
BACKUP_DIR=./backups
BACKUP_REMOTE=
BACKUP_RETENTION_DAYS=14
# --- Supplier adapters (design doc §04) ---
# staging: gutenberg_test production: booksrun,ingram
ACTIVE_SUPPLIER_ADAPTERS=gutenberg_test
BOOKSRUN_API_KEY=
INGRAM_API_KEY=
INGRAM_ACCOUNT_ID=
# --- Payments (Helcim) ---
HELCIM_API_TOKEN=
HELCIM_ACCOUNT_ID=
# --- Marketing ---
MAILERLITE_API_KEY=
+9
View File
@@ -0,0 +1,9 @@
/.env
/.env.*
!/.env.example
/backups/
vendor/
node_modules/
*.log
.DS_Store
+29
View File
@@ -0,0 +1,29 @@
ENV ?= dev
COMPOSE = docker compose -f docker-compose.yml -f docker-compose.$(ENV).yml --env-file .env.$(ENV)
.PHONY: up down ps logs shell wp deploy backup
up:
$(COMPOSE) up -d --build
down:
$(COMPOSE) down
ps:
$(COMPOSE) ps
logs:
$(COMPOSE) logs -f
shell:
$(COMPOSE) exec wordpress bash
# make wp ENV=staging ARGS="plugin list"
wp:
$(COMPOSE) exec wordpress wp $(ARGS) --allow-root
deploy:
./deploy/deploy.sh $(ENV)
backup:
./deploy/backup.sh $(ENV)
+122
View File
@@ -0,0 +1,122 @@
# Bookstore
WordPress + WooCommerce bookstore. Design reference: see `docs/` for the
pre-launch plan and the technical design doc (`bookstore-core` schema,
interfaces, order state machine — the design doc is the source of truth for
*why* this repo is laid out the way it is).
This week's scope (Week 1 of the 5-week plan): environments, deploy
pipeline, and the WooCommerce/HPOS plugin scaffold. Catalog schema, pricing,
supplier adapters, and the order state machine are Week 2+ and live under
`wp-content/plugins/bookstore-core/includes/`, currently empty.
## Layout
```
docker-compose.yml base services: db, redis, wordpress, cron
docker-compose.{dev,staging,production}.yml per-environment overrides (caddy, mailhog)
docker/php/ app image: WP core image + wp-cli, composer, redis ext
docker/caddy/ one Caddyfile per environment
docker/cron/ Action Scheduler driver (system cron has no host to run on in Docker)
deploy/deploy.sh idempotent bring-up + WP/WooCommerce config
deploy/hooks/post-receive git-server hook: push to `staging`/`main` deploys
deploy/backup.sh, restore.sh off-host backup; restore is staging-only, on purpose
wp-content/plugins/bookstore-core/ the one plugin that owns business logic
wp-content/mu-plugins/ local-mail-catcher.php — routes mail to MailHog outside production
```
Same code runs in every environment; only `.env.<env>` and which
`docker-compose.<env>.yml` you layer in differ (design doc §01).
## Local development
```
cp .env.example .env.dev # fill in DB_PASSWORD etc.; localhost values are fine
make up ENV=dev
make deploy ENV=dev # installs WP, WooCommerce, activates bookstore-core, enables HPOS
```
Site is at `http://localhost:8080`. `make wp ENV=dev ARGS="plugin list"` runs
any wp-cli command; `make logs ENV=dev` tails everything; `make shell ENV=dev`
drops into the app container.
## Staging (this is what goes on your Docker server)
```
cp .env.example .env.staging
# fill in SITE_DOMAIN, SITE_URL, DB_*, and:
docker run --rm caddy:2-alpine caddy hash-password --plaintext 'pick-a-password'
# -> paste result into STAGING_BASIC_AUTH_HASH
make deploy ENV=staging
```
Staging is `noindex`'d and sits behind HTTP basic auth (Caddyfile.staging)
in addition to `blog_public=0` — two independent reasons search engines and
random visitors won't see it, per the launch gate. Mail never leaves the
box: it's caught by MailHog, viewable at `:8025`.
### Theme: Blocksy + the Book Store starter site
`deploy.sh` installs and activates the **Blocksy** theme and the free
**Blocksy Companion** plugin from wordpress.org automatically — nothing to
do here, in any environment.
The **Book Store starter site** itself is a paid Companion Pro template
(Business plan, $99/year — design doc Appendix A), so it can't be scripted
against a public API the way the free theme can. One-time manual step per
environment, in wp-admin:
1. Purchase/retrieve the Blocksy Business license key.
2. **Blocksy → General → License** → activate it.
3. **Blocksy → Extensions → Starter Sites** (Companion Pro) → import
**Book Store**.
After that, the starter site's content and Customizer settings persist in
the database like any other WordPress content — a redeploy or a fresh
`deploy.sh` run doesn't touch it or need to repeat it.
## Deploy pipeline
This assumes your own bare Git server, not a hosted CI. The mechanism:
1. A bare repo lives on the server (e.g. `/srv/git/bookstore.git`).
2. `deploy/hooks/post-receive` is copied into `<bare-repo>.git/hooks/` and
made executable. Edit `DEPLOY_ROOT` at the top of it first.
3. Pushing to `staging` checks that branch out into
`$DEPLOY_ROOT/staging` and runs `deploy.sh staging`; pushing to `main`
deploys `$DEPLOY_ROOT/production` the same way.
4. `deploy.sh` is idempotent — it's safe to re-run and safe to be the thing
the hook calls on every push.
You can also just run `./deploy/deploy.sh <env>` by hand from a checked-out
copy on the server; the hook is only automation on top of the same script.
## Backups
```
./deploy/backup.sh staging # or production
```
Dumps the database and archives `wp-content/uploads`, gzips both, and syncs
to `$BACKUP_REMOTE` via `rclone` if set (configure `rclone config` on the
server first — this repo doesn't manage remote credentials). Before launch,
run a real restore drill:
```
./deploy/restore.sh ./backups/production/db-<ts>.sql.gz ./backups/production/uploads-<ts>.tar.gz
```
`restore.sh` only ever targets staging — there's no production argument, so
a restore drill can't accidentally overwrite the live site.
## What's deliberately not here yet
- Catalog tables, pricing engine, supplier adapters, order state machine —
Week 24, see the design doc.
- A real TLS-terminating public domain — Caddy will auto-provision certs
once `SITE_DOMAIN` points at a real host with 80/443 reachable; until
then `docker-compose.dev.yml` is the only one that works from
`localhost`.
- CI test/lint automation — nothing here runs tests yet because there's no
application code to test yet.
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Off-host database + uploads backup (design doc §01, launch gate: "A full
# backup has been restored successfully into staging" — see restore.sh).
set -euo pipefail
ENVIRONMENT="${1:?Usage: backup.sh <staging|production>}"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
ENV_FILE=".env.${ENVIRONMENT}"
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
BACKUP_DIR="${BACKUP_DIR:-./backups}/${ENVIRONMENT}"
mkdir -p "$BACKUP_DIR"
COMPOSE="docker compose -f docker-compose.yml -f docker-compose.${ENVIRONMENT}.yml --env-file ${ENV_FILE}"
echo "==> dumping database"
$COMPOSE exec -T db sh -c "exec mysqldump -u\"\$MARIADB_USER\" -p\"\$MARIADB_PASSWORD\" \"\$MARIADB_DATABASE\"" \
| gzip > "$BACKUP_DIR/db-${TIMESTAMP}.sql.gz"
echo "==> archiving uploads"
$COMPOSE run --rm -T wordpress tar -czf - -C /var/www/html/wp-content uploads \
> "$BACKUP_DIR/uploads-${TIMESTAMP}.tar.gz"
if [[ -n "${BACKUP_REMOTE:-}" ]]; then
echo "==> syncing to off-host storage ($BACKUP_REMOTE)"
rclone copy "$BACKUP_DIR/db-${TIMESTAMP}.sql.gz" "$BACKUP_REMOTE/${ENVIRONMENT}/"
rclone copy "$BACKUP_DIR/uploads-${TIMESTAMP}.tar.gz" "$BACKUP_REMOTE/${ENVIRONMENT}/"
else
echo "==> BACKUP_REMOTE not set — backup stayed local only; configure rclone before launch"
fi
echo "==> pruning local backups older than ${BACKUP_RETENTION_DAYS:-14} days"
find "$BACKUP_DIR" -type f -mtime "+${BACKUP_RETENTION_DAYS:-14}" -delete
echo "==> backup complete: $BACKUP_DIR"
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# Builds and brings up an environment, then makes sure WordPress/WooCommerce
# are installed and configured to match design doc §01/§03/§07. Safe to run
# repeatedly — every step is idempotent.
set -euo pipefail
ENVIRONMENT="${1:?Usage: deploy.sh <dev|staging|production>}"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
case "$ENVIRONMENT" in
dev|staging|production) ;;
*) echo "environment must be 'dev', 'staging', or 'production'" >&2; exit 1 ;;
esac
ENV_FILE=".env.${ENVIRONMENT}"
if [[ ! -f "$ENV_FILE" ]]; then
echo "missing $ENV_FILE — copy .env.example to $ENV_FILE and fill it in" >&2
exit 1
fi
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
COMPOSE="docker compose -f docker-compose.yml -f docker-compose.${ENVIRONMENT}.yml --env-file ${ENV_FILE}"
echo "==> building and starting ${ENVIRONMENT}"
$COMPOSE up -d --build
echo "==> waiting for the wordpress container to come up"
for i in $(seq 1 30); do
if $COMPOSE exec -T wordpress php -v >/dev/null 2>&1; then
break
fi
if [[ "$i" -eq 30 ]]; then
echo "wordpress container did not become ready in time" >&2
exit 1
fi
sleep 2
done
echo "==> ensuring WordPress is installed"
if ! $COMPOSE exec -T wordpress wp core is-installed --allow-root; then
$COMPOSE exec -T wordpress wp core install \
--url="${SITE_URL:?set SITE_URL in ${ENV_FILE}}" \
--title="${SITE_TITLE:-Bookstore}" \
--admin_user="${WP_ADMIN_USER:?set WP_ADMIN_USER in ${ENV_FILE}}" \
--admin_password="${WP_ADMIN_PASSWORD:?set WP_ADMIN_PASSWORD in ${ENV_FILE}}" \
--admin_email="${WP_ADMIN_EMAIL:?set WP_ADMIN_EMAIL in ${ENV_FILE}}" \
--skip-email \
--allow-root
fi
echo "==> installing/activating theme (Blocksy — free; Book Store starter site needs a manual license + import, see README)"
$COMPOSE exec -T wordpress wp theme install blocksy --activate --allow-root
echo "==> installing/activating required plugins"
$COMPOSE exec -T wordpress wp plugin install woocommerce redis-cache blocksy-companion --activate --allow-root
$COMPOSE exec -T wordpress wp plugin activate bookstore-core --allow-root
$COMPOSE exec -T wordpress wp redis enable --allow-root || true
echo "==> enabling WooCommerce HPOS (custom order tables)"
$COMPOSE exec -T wordpress wp option update woocommerce_custom_orders_table_enabled yes --allow-root
$COMPOSE exec -T wordpress wp option update woocommerce_custom_orders_table_data_sync_enabled yes --allow-root
if [[ "$ENVIRONMENT" != "production" ]]; then
echo "==> discouraging search engines (non-production)"
$COMPOSE exec -T wordpress wp option update blog_public 0 --allow-root
fi
echo "==> flushing caches and rewrite rules"
$COMPOSE exec -T wordpress wp cache flush --allow-root
$COMPOSE exec -T wordpress wp rewrite flush --allow-root
echo "==> deploy complete: ${ENVIRONMENT}"
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Install this at <bare-repo>.git/hooks/post-receive on your own Git server
# (chmod +x it) and adjust DEPLOY_ROOT below. Pushing to `staging` deploys
# to the staging checkout; pushing to `main` deploys to production.
#
# git remote add origin <you>@<git-server>:/srv/git/bookstore.git
# git push origin staging
#
set -euo pipefail
DEPLOY_ROOT="/srv/bookstore"
while read -r oldrev newrev refname; do
branch="${refname#refs/heads/}"
case "$branch" in
staging) target="$DEPLOY_ROOT/staging"; env="staging" ;;
main) target="$DEPLOY_ROOT/production"; env="production" ;;
*) echo "post-receive: ignoring push to $branch"; continue ;;
esac
echo "post-receive: deploying $branch -> $env ($target)"
mkdir -p "$target"
git --work-tree="$target" --git-dir="$(pwd)" checkout -f "$branch"
( cd "$target" && ./deploy/deploy.sh "$env" )
done
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Restores a backup INTO STAGING ONLY. Deliberately has no production path —
# the launch gate is "a full backup has been restored into staging," never
# production overwritten by a drill.
set -euo pipefail
DB_DUMP="${1:?Usage: restore.sh <db-dump.sql.gz> <uploads.tar.gz>}"
UPLOADS_ARCHIVE="${2:?Usage: restore.sh <db-dump.sql.gz> <uploads.tar.gz>}"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
ENV_FILE=".env.staging"
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
COMPOSE="docker compose -f docker-compose.yml -f docker-compose.staging.yml --env-file ${ENV_FILE}"
echo "!! this OVERWRITES the staging database and uploads !!"
read -r -p "type 'restore' to continue: " confirm
[[ "$confirm" == "restore" ]] || { echo "aborted"; exit 1; }
echo "==> restoring database"
gunzip -c "$DB_DUMP" | $COMPOSE exec -T db sh -c "exec mysql -u\"\$MARIADB_USER\" -p\"\$MARIADB_PASSWORD\" \"\$MARIADB_DATABASE\""
echo "==> restoring uploads"
ARCHIVE_DIR="$(cd "$(dirname "$UPLOADS_ARCHIVE")" && pwd)"
ARCHIVE_NAME="$(basename "$UPLOADS_ARCHIVE")"
$COMPOSE run --rm -T -v "${ARCHIVE_DIR}:/restore:ro" wordpress \
tar -xzf "/restore/${ARCHIVE_NAME}" -C /var/www/html/wp-content
echo "==> restore complete — verify the site before treating the drill as passed"
+29
View File
@@ -0,0 +1,29 @@
x-env-type: &env-type
WP_ENVIRONMENT_TYPE: local
services:
wordpress:
environment: *env-type
cron:
environment: *env-type
caddy:
image: caddy:2-alpine
restart: unless-stopped
depends_on:
- wordpress
ports:
- "8080:80"
volumes:
- ./docker/caddy/Caddyfile.dev:/etc/caddy/Caddyfile:ro
- wp_core:/var/www/html:ro
- wp_uploads:/var/www/html/wp-content/uploads:ro
- wp_themes:/var/www/html/wp-content/themes:ro
- ./wp-content/plugins/bookstore-core:/var/www/html/wp-content/plugins/bookstore-core:ro
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
+32
View File
@@ -0,0 +1,32 @@
x-env-type: &env-type
WP_ENVIRONMENT_TYPE: production
services:
wordpress:
environment: *env-type
cron:
environment: *env-type
caddy:
image: caddy:2-alpine
restart: unless-stopped
depends_on:
- wordpress
ports:
- "80:80"
- "443:443"
environment:
SITE_DOMAIN: ${SITE_DOMAIN}
volumes:
- ./docker/caddy/Caddyfile.production:/etc/caddy/Caddyfile:ro
- wp_core:/var/www/html:ro
- wp_uploads:/var/www/html/wp-content/uploads:ro
- wp_themes:/var/www/html/wp-content/themes:ro
- ./wp-content/plugins/bookstore-core:/var/www/html/wp-content/plugins/bookstore-core:ro
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
+40
View File
@@ -0,0 +1,40 @@
x-env-type: &env-type
WP_ENVIRONMENT_TYPE: staging
services:
wordpress:
environment: *env-type
cron:
environment: *env-type
caddy:
image: caddy:2-alpine
restart: unless-stopped
depends_on:
- wordpress
ports:
- "80:80"
- "443:443"
environment:
SITE_DOMAIN: ${SITE_DOMAIN}
STAGING_BASIC_AUTH_USER: ${STAGING_BASIC_AUTH_USER}
STAGING_BASIC_AUTH_HASH: ${STAGING_BASIC_AUTH_HASH}
volumes:
- ./docker/caddy/Caddyfile.staging:/etc/caddy/Caddyfile:ro
- wp_core:/var/www/html:ro
- wp_uploads:/var/www/html/wp-content/uploads:ro
- wp_themes:/var/www/html/wp-content/themes:ro
- ./wp-content/plugins/bookstore-core:/var/www/html/wp-content/plugins/bookstore-core:ro
- caddy_data:/data
- caddy_config:/config
mailhog:
image: mailhog/mailhog:v1.0.1
restart: unless-stopped
ports:
- "8025:8025"
volumes:
caddy_data:
caddy_config:
+95
View File
@@ -0,0 +1,95 @@
# wordpress and cron are the same image and MUST share the same
# environment: the official image's wp-config.php reads getenv() live, at
# PHP runtime, in whichever container is executing — it does not bake
# values into the file once. If cron's env drifts from wordpress's (e.g.
# WORDPRESS_CONFIG_EXTRA missing), cron silently loses WP_REDIS_HOST,
# DISABLE_WP_CRON, etc. The anchor below is what prevents that drift.
x-bookstore-env: &bookstore-env
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: ${DB_NAME}
WORDPRESS_DB_USER: ${DB_USER}
WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
WORDPRESS_TABLE_PREFIX: ${WP_TABLE_PREFIX:-wp_}
MAIL_CATCHER_HOST: mailhog
MAIL_CATCHER_PORT: 1025
WORDPRESS_CONFIG_EXTRA: |
define('DISABLE_WP_CRON', true);
define('WP_REDIS_HOST', 'redis');
define('WP_MEMORY_LIMIT', '256M');
# Passed through for the bookstore-core plugin's supplier adapters /
# payment integration (design doc §04/§05) — not read by WordPress core.
ACTIVE_SUPPLIER_ADAPTERS: ${ACTIVE_SUPPLIER_ADAPTERS:-gutenberg_test}
BOOKSRUN_API_KEY: ${BOOKSRUN_API_KEY:-}
INGRAM_API_KEY: ${INGRAM_API_KEY:-}
INGRAM_ACCOUNT_ID: ${INGRAM_ACCOUNT_ID:-}
HELCIM_API_TOKEN: ${HELCIM_API_TOKEN:-}
HELCIM_ACCOUNT_ID: ${HELCIM_ACCOUNT_ID:-}
MAILERLITE_API_KEY: ${MAILERLITE_API_KEY:-}
services:
db:
image: mariadb:11
restart: unless-stopped
environment:
MARIADB_DATABASE: ${DB_NAME}
MARIADB_USER: ${DB_USER}
MARIADB_PASSWORD: ${DB_PASSWORD}
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -u$$MARIADB_USER -p$$MARIADB_PASSWORD --silent"]
interval: 5s
timeout: 5s
retries: 20
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20
wordpress:
build:
context: ./docker/php
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
environment: *bookstore-env
volumes:
- wp_core:/var/www/html
- wp_uploads:/var/www/html/wp-content/uploads
- wp_themes:/var/www/html/wp-content/themes
- ./wp-content/plugins/bookstore-core:/var/www/html/wp-content/plugins/bookstore-core
- ./wp-content/mu-plugins:/var/www/html/wp-content/mu-plugins
cron:
build:
context: ./docker/php
restart: unless-stopped
depends_on:
- wordpress
entrypoint: ["/bin/sh", "/usr/local/bin/cron-entrypoint.sh"]
environment: *bookstore-env
volumes:
- wp_core:/var/www/html
- wp_uploads:/var/www/html/wp-content/uploads
- wp_themes:/var/www/html/wp-content/themes
- ./wp-content/plugins/bookstore-core:/var/www/html/wp-content/plugins/bookstore-core
- ./wp-content/mu-plugins:/var/www/html/wp-content/mu-plugins
- ./docker/cron/entrypoint.sh:/usr/local/bin/cron-entrypoint.sh:ro
volumes:
db_data:
redis_data:
wp_core:
wp_uploads:
wp_themes:
+10
View File
@@ -0,0 +1,10 @@
:80 {
root * /var/www/html
request_body {
max_size 32MB
}
php_fastcgi wordpress:9000
file_server
}
+22
View File
@@ -0,0 +1,22 @@
{$SITE_DOMAIN} {
encode gzip
root * /var/www/html
@no_cache path /cart* /checkout* /my-account* /wp-admin* /wp-login.php
header @no_cache Cache-Control "no-store, no-cache, must-revalidate"
request_body {
max_size 32MB
}
php_fastcgi wordpress:9000
file_server
log {
output file /data/access.log {
roll_size 50MiB
roll_keep 10
}
}
}
+22
View File
@@ -0,0 +1,22 @@
{$SITE_DOMAIN} {
encode gzip
# Launch gate: "No staging URLs are publicly indexed." Belt-and-suspenders
# with wp_option blog_public=0, which deploy.sh sets on staging.
basic_auth {
{$STAGING_BASIC_AUTH_USER} {$STAGING_BASIC_AUTH_HASH}
}
header X-Robots-Tag "noindex, nofollow, noarchive"
root * /var/www/html
@no_cache path /cart* /checkout* /my-account* /wp-admin* /wp-login.php
header @no_cache Cache-Control "no-store, no-cache, must-revalidate"
request_body {
max_size 32MB
}
php_fastcgi wordpress:9000
file_server
}
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
# System-cron replacement for Action Scheduler / WP-Cron (design doc §01).
# WORDPRESS_CONFIG_EXTRA sets DISABLE_WP_CRON, so this loop is the only
# thing driving scheduled events in every environment.
set -eu
echo "bookstore-core cron sidecar starting (60s interval)"
while true; do
if ! wp cron event run --due-now --path=/var/www/html --allow-root 2>/tmp/cron-last-error.log; then
echo "cron run failed: $(cat /tmp/cron-last-error.log)"
fi
sleep 60
done
+23
View File
@@ -0,0 +1,23 @@
# PHP-FPM + WordPress core, plus the tooling deploy.sh and the cron
# sidecar need: wp-cli, composer, and the redis extension for object cache.
FROM wordpress:php8.3-fpm
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
unzip \
less \
default-mysql-client \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
RUN curl -o /usr/local/bin/wp -sSL \
https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
&& chmod +x /usr/local/bin/wp
COPY php.ini /usr/local/etc/php/conf.d/zz-bookstore.ini
WORKDIR /var/www/html
+13
View File
@@ -0,0 +1,13 @@
[PHP]
upload_max_filesize = 32M
post_max_size = 32M
memory_limit = 256M
max_execution_time = 120
max_input_vars = 3000
[opcache]
opcache.enable = 1
opcache.memory_consumption = 128
opcache.max_accelerated_files = 10000
opcache.validate_timestamps = 1
opcache.revalidate_freq = 2
@@ -0,0 +1,22 @@
<?php
/**
* Route outgoing mail to a local catcher outside production, so no test
* email ever reaches a real customer (design doc §01/§08).
*
* Same code in every environment — the only thing that changes is the
* WP_ENVIRONMENT_TYPE define set per-environment in docker-compose.
*/
defined('ABSPATH') || exit;
if (wp_get_environment_type() === 'production') {
return;
}
add_action('phpmailer_init', function ($phpmailer) {
$phpmailer->isSMTP();
$phpmailer->Host = getenv('MAIL_CATCHER_HOST') ?: 'mailhog';
$phpmailer->Port = getenv('MAIL_CATCHER_PORT') ?: 1025;
$phpmailer->SMTPAuth = false;
$phpmailer->SMTPAutoTLS = false;
});
@@ -0,0 +1,38 @@
<?php
/**
* Plugin Name: Bookstore Core
* Description: Owns catalog, pricing, supplier routing, and order-state business logic for the bookstore. See docs/ for the technical design document.
* Version: 0.1.0
* Requires PHP: 8.1
* Requires Plugins: woocommerce
* Author: Bookstore
*/
defined('ABSPATH') || exit;
define('BSC_PLUGIN_FILE', __FILE__);
define('BSC_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('BSC_VERSION', '0.1.0');
// Declare HPOS (custom order tables) compatibility per design doc §01/§06 —
// the order state machine and audit tables are built against HPOS, not
// legacy post-based orders.
add_action('before_woocommerce_init', function () {
if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);
}
});
register_activation_hook(__FILE__, function () {
if (!class_exists('WooCommerce')) {
deactivate_plugins(plugin_basename(__FILE__));
wp_die('Bookstore Core requires WooCommerce to be installed and active.');
}
});
// Catalog schema, pricing engine, supplier adapters, and the order state
// machine (design doc §02–§07) are built here starting Week 2.
@@ -0,0 +1,13 @@
{
"name": "bookstore/bookstore-core",
"description": "Business logic plugin for the bookstore: catalog, pricing, supplier routing, order state.",
"type": "wordpress-plugin",
"require": {
"php": ">=8.1"
},
"autoload": {
"psr-4": {
"Bookstore\\Core\\": "includes/"
}
}
}