Files
bookstore/docker-compose.yml
T
twooeyandClaude Sonnet 5 a733ba4bfd Week 4 (scoped): BooksRun live price/stock check + daily FTP sync stub
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
2026-08-27 21:29:37 -04:00

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: