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:
@@ -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=
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/.env
|
||||||
|
/.env.*
|
||||||
|
!/.env.example
|
||||||
|
|
||||||
|
/backups/
|
||||||
|
vendor/
|
||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -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)
|
||||||
@@ -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 2–4, 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.
|
||||||
Executable
+41
@@ -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"
|
||||||
Executable
+76
@@ -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}"
|
||||||
Executable
+26
@@ -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
|
||||||
Executable
+33
@@ -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"
|
||||||
@@ -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:
|
||||||
@@ -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:
|
||||||
@@ -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:
|
||||||
@@ -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:
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
:80 {
|
||||||
|
root * /var/www/html
|
||||||
|
|
||||||
|
request_body {
|
||||||
|
max_size 32MB
|
||||||
|
}
|
||||||
|
|
||||||
|
php_fastcgi wordpress:9000
|
||||||
|
file_server
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Executable
+14
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Binary file not shown.
@@ -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/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user