Adds a live per-ISBN BooksRun API check to the checkout-time price recheck (Order\PriceRecheck) so a booksrun-sourced offer is verified fresh, not trusted from a potentially day-stale local cache -- fails closed on any problem (API error, no stock), matching Appendix B's "stale supplier cost must never be trusted as final." Live values are written back to the same offer_id row so SupplierOrderJob's later fulfillment-time read sees them too. Also wires a real daily Action Scheduler job (scheduling/dedup/batch-write infrastructure) for BooksRun's FTP inventory feed, but leaves the actual connect+parse step (Import\BooksRunOfferSync::connect_and_parse_feed()) as a deliberate stub -- no FTP access or sample feed exists yet, and guessing at a file format would silently write wrong data once real access arrives. The stub fails loud on the manual CLI trigger, quiet on the scheduled job (no point alarming on an expected, known state). Ingram, BooksRun AutoBuy, production email, and tax/legal/monitoring are explicitly out of scope for this pass -- narrowed by user request. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2gmgaDVXcRGCwLfdXQajD
179 lines
8.7 KiB
YAML
179 lines
8.7 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 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,
|
|
# HELCIM_API_TOKEN (Week 3), and BOOKSRUN_API_KEY/BOOKSRUN_FTP_PASSWORD
|
|
# (Week 4) now do 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}
|
|
INGRAM_API_KEY: ${INGRAM_API_KEY:-}
|
|
INGRAM_ACCOUNT_ID: ${INGRAM_ACCOUNT_ID:-}
|
|
HELCIM_ACCOUNT_ID: ${HELCIM_ACCOUNT_ID:-}
|
|
MAILERLITE_API_KEY: ${MAILERLITE_API_KEY:-}
|
|
HARDCOVER_API_TOKEN_FILE: /run/secrets/hardcover_api_token
|
|
HELCIM_API_TOKEN_FILE: /run/secrets/helcim_api_token
|
|
BOOKSRUN_API_KEY_FILE: /run/secrets/booksrun_api_key
|
|
# Week 4: bulk FTP inventory feed connection details — BOOKSRUN_FTP_HOST
|
|
# empty means Import\BooksRunOfferSync::is_ftp_configured() is false and
|
|
# the daily sync job stays a no-op (see that class). None of these are
|
|
# read by any code path yet beyond that one boolean check — the actual
|
|
# connect+parse logic is still a stub pending real access/a sample feed.
|
|
BOOKSRUN_FTP_HOST: ${BOOKSRUN_FTP_HOST:-}
|
|
BOOKSRUN_FTP_PORT: ${BOOKSRUN_FTP_PORT:-}
|
|
BOOKSRUN_FTP_USER: ${BOOKSRUN_FTP_USER:-}
|
|
BOOKSRUN_FTP_REMOTE_PATH: ${BOOKSRUN_FTP_REMOTE_PATH:-}
|
|
BOOKSRUN_FTP_PASSWORD_FILE: /run/secrets/booksrun_ftp_password
|
|
|
|
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
|
|
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/helcim_api_token:/run/secrets/helcim_api_token:ro
|
|
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/booksrun_api_key:/run/secrets/booksrun_api_key:ro
|
|
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/booksrun_ftp_password:/run/secrets/booksrun_ftp_password: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
|
|
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/helcim_api_token:/run/secrets/helcim_api_token:ro
|
|
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/booksrun_api_key:/run/secrets/booksrun_api_key:ro
|
|
- ./secrets/${ENVIRONMENT:?ENVIRONMENT must be set}/booksrun_ftp_password:/run/secrets/booksrun_ftp_password:ro
|
|
|
|
volumes:
|
|
db_data:
|
|
redis_data:
|
|
wp_core:
|
|
wp_uploads:
|
|
wp_themes:
|