Files
bookstore/docker-compose.yml
T
twooey 9253c6638f Fix a real regression plus three hardening gaps found in a follow-up review
A second review pass specifically targeted at the fixes just made (not the
original codebase) — caught one genuine regression from this session's own
earlier work, plus a couple of gaps the first pass didn't probe deeply
enough to find.

- REGRESSION: SupplierOffer::insert() throwing on failure (a Medium-severity
  fix earlier this session, matching Work/Edition/Isbn) was never given a
  catch anywhere in the offer-generation path. Before that fix, a bad
  insert silently continued; after it, nothing stopped the exception from
  aborting the ENTIRE generate-offers run on the first failure. Fixed with
  the same per-item catch pattern GutenbergImporter already established.
  Verified by forcing a real DB failure (renamed the table mid-run): before
  this fix that aborted the batch immediately; after, it correctly
  processed all 2000 ISBNs, reported each failure individually, and
  completed with an accurate failed count — table restored after, 2000
  offers confirmed intact.

- ProductSync::sync_one() never checked WC_Product::save()'s return value.
  Verified directly (forced via the wp_insert_post_empty_content filter):
  save() returns 0 rather than throwing on a wp_insert_post()-level
  rejection, which would have gone through as Work::set_product_id($id, 0)
  — silently "succeeding" and invisible in $failed[], inconsistent with
  every other failure mode in this method already routing through the
  per-product SAVEPOINT + $failed[] reporting added this session. Now
  throws instead, confirmed it does NOT corrupt the existing pointer (stays
  at its prior value, self-heals on a future successful run) rather than
  writing 0.

- docker-compose.yml: the wordpress healthcheck's timeout budget (~130s)
  could plausibly be exceeded by a legitimately slow (not broken) first
  boot on the 2-core/8GB box this actually runs on — confirmed that a
  service_healthy dependency timing out makes `docker compose up -d` (and
  so deploy.sh, under set -euo pipefail) hard-fail rather than just start
  late, a new deploy failure mode this session's healthcheck introduced.
  Widened to ~4.5 minutes of headroom (retries 20->40, start_period
  30s->60s), well above deploy.sh's own existing 120s precedent for just
  the core-file-copy portion of the same boot.

- Makefile: the per-target `; rc=$?; rm -f ...; exit $rc` cleanup added
  earlier this session doesn't reliably run under a real Ctrl+C — a
  foreground SIGINT terminates that shell chain before the `;` continues.
  Switched to a `trap 'rm -f ...' EXIT` (matching the idiom deploy.sh/
  backup.sh already use), which fires on any shell termination and doesn't
  need to manually thread the exit code through. Verified by actually
  sending SIGINT to the whole process group (matching real terminal Ctrl+C,
  not just a backgrounded job) during `make logs` — the compose env file
  was correctly removed.

Also cleaned up one duplicate leftover product (work_id=1 briefly had two
products, from repeated manual test scenarios reusing the same work_id
across this long session's testing, not from an active bug — confirmed the
current code produces 0 new duplicates on repeated sync-products runs)
found while verifying the ProductSync fix above.
2026-08-27 16:51:58 -04:00

162 lines
7.4 KiB
YAML

# 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.
# DB_PASSWORD/DB_ROOT_PASSWORD are deliberately NOT passed via ${VAR} here.
# Docker Compose's own interpolation mangles any value containing a `$`
# followed by a letter (confirmed with a bcrypt hash earlier, and again with
# a plain generated password: "$BRjTx3tlmSpz" got silently blanked out,
# breaking the DB connection). The official mariadb/wordpress images both
# support a `_FILE` convention specifically for this — point at a file path
# (never risky) and the container reads the raw contents itself, bypassing
# Compose's interpolation entirely. deploy.sh writes these files fresh from
# .env.<environment> before every `up`.
#
# These are plain bind mounts, NOT Compose's native `secrets:` block —
# Compose's `secrets:` construct reads the referenced file's *content* as
# part of its own config model and applies the same interpolation warning
# to it (confirmed: it still warned on "$BRjTx3tlmSpz" even after that
# value was fully removed from every ${VAR} and --env-file path). A plain
# bind mount never touches the file's content, only its path, so it's
# immune to this entirely.
#
# The remaining supplier/payment keys below are still passed the old ${VAR}
# way and remain exposed to the same class of bug — nothing consumes them
# yet (bookstore-core is still a stub for those), so when that code is
# written it should read `_FILE` variants the same way HARDCOVER_API_TOKEN
# now does below.
x-bookstore-env: &bookstore-env
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: ${DB_NAME}
WORDPRESS_DB_USER: ${DB_USER}
WORDPRESS_DB_PASSWORD_FILE: /run/secrets/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:-}
HARDCOVER_API_TOKEN_FILE: /run/secrets/hardcover_api_token
services:
db:
image: mariadb:11
restart: unless-stopped
environment:
MARIADB_DATABASE: ${DB_NAME}
MARIADB_USER: ${DB_USER}
MARIADB_PASSWORD_FILE: /run/secrets/db_password
MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
volumes:
- db_data:/var/lib/mysql
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/db_password:/run/secrets/db_password:ro
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/db_root_password:/run/secrets/db_root_password:ro
healthcheck:
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -u$$MARIADB_USER -p\"$$(cat /run/secrets/db_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
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/db_password:/run/secrets/db_password:ro
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/hardcover_api_token:/run/secrets/hardcover_api_token:ro
healthcheck:
# No HTTP endpoint to hit directly — php-fpm speaks FastCGI on 9000,
# not HTTP, and this image has no `nc`. bash's /dev/tcp redirection
# needs no extra binary and is enough to know php-fpm is actually
# accepting connections (verified directly: exits 0 once php-fpm is up,
# 1 against a closed port). Without this, cron/caddy's depends_on only
# waited for the container to *start*, not for php-fpm inside it to be
# ready — a slow first boot (official image copying core files in, could
# be worse on modest hardware) could let Caddy serve a transient 502
# before php-fpm was actually listening.
#
# cron and caddy now gate on this via depends_on: condition:
# service_healthy, which makes `docker compose up -d` (and so
# deploy.sh, under set -euo pipefail) hard-fail if wordpress never
# reports healthy in time — a new way for a deploy to fail that didn't
# exist before this healthcheck did. Budgeted generously (~4.5 min:
# start_period + retries*interval) for the 2-core/8GB box this
# actually runs on: deploy.sh's own wp-settings.php wait loop already
# budgets up to 120s just for the image's core-file copy step (chosen
# for this same hardware), and this check additionally waits for
# php-fpm itself to start, plus whatever `--build` is competing for on
# a 2-core box. A too-tight budget here would turn "slow but fine" into
# a spurious deploy failure, not just a slower one.
test: ["CMD-SHELL", "bash -c '(exec 3<>/dev/tcp/127.0.0.1/9000)' 2>/dev/null"]
interval: 5s
timeout: 3s
retries: 40
start_period: 60s
cron:
build:
context: ./docker/php
restart: unless-stopped
# Safe here (unlike wordpress's own service): this is a plain shell loop,
# not php-fpm's master process, which needs to start as root so it can
# drop privileges to its www-data workers itself. Missed in the earlier
# root-hardening pass — found via a real bug: this container writing
# wp-cron-triggered uploads as root, silently blocking www-data-run
# wp-cli commands from writing new files into those same directories.
user: www-data
depends_on:
wordpress:
condition: service_healthy
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
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/db_password:/run/secrets/db_password:ro
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/hardcover_api_token:/run/secrets/hardcover_api_token:ro
volumes:
db_data:
redis_data:
wp_core:
wp_uploads:
wp_themes: