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