Files
bookstore/docker-compose.yml
T
twooey e4ee584e86 Fix six low-severity issues from the full-session code review
- PricingEngine::round_to_99(): ceil($price) - 0.01 undershoots whenever
  $price's cents are already .99 or higher — an exact integer (ceil()
  equals floor(), landing a full cent below $price) or, more subtly, any
  fractional price above X.99 itself (a division result, not something
  pre-rounded to 2 decimals, e.g. 20.995 -> old formula gave 20.99, below
  the input). Rewritten as floor()+0.99, bumped by 1 if still under $price.
  Verified against 8 cases including both boundary classes: every result
  now >= its input.

- CoverSync::attach_cover(): wp_generate_attachment_metadata()'s return
  value was never checked. Assumed it'd return empty on failure — verified
  directly it does NOT: fed it 2000 bytes of garbage and got back
  ['filesize' => 2000], no width/height, since GD/Imagick couldn't decode
  it. Old code would report "attached" for a degraded image with no
  dimensions/srcset. Now checks for width+height specifically, and cleans
  up the orphaned attachment on failure so a re-run retries the product.
  Verified both the corrupt-image rejection (no orphan left, no thumbnail
  set) and that a real image still attaches normally.

- Makefile: the per-invocation .env.$(ENV).compose file (holds every API
  key and both DB passwords, stripped of DB_PASSWORD/DB_ROOT_PASSWORD only)
  was never cleaned up, left at default 644 in the repo root after every
  `make` command. Now chmod 600 on creation and removed at the end of every
  target, preserving the underlying command's exit code through the
  cleanup. Verified both the happy path (file gone after, exit 0) and the
  failure path (bad ENV: file still cleaned up, real exit code still
  propagates through make).

- docker-compose.yml: added a healthcheck to the wordpress service (bash's
  /dev/tcp against php-fpm's port 9000 — no HTTP endpoint to hit directly,
  and no `nc` in this image; verified it correctly succeeds once php-fpm is
  listening and fails against a closed port) and switched cron's and
  caddy's depends_on (across all three env overlays) from bare
  container-started to condition: service_healthy. Previously both could
  start against a wordpress container that had started but wasn't actually
  ready yet. Verified via a full down/up cycle: db+redis healthy, then
  wordpress starts and becomes healthy, only then do cron and caddy start.

- .env.dev/.env.staging/.env.production chmod'd 600 (were 644) — same
  plaintext-credential content as secrets/<env>/, which is already 700/644
  at the directory/file level respectively for a different reason (container
  UID readability); these have no such constraint, only the host CLI reads
  them. Also removed a stray .env.staging.compose left over from before the
  Makefile fix above existed. Noted the convention in .env.example so newly
  created env files follow it too.
2026-08-27 15:57:13 -04:00

149 lines
6.5 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.
test: ["CMD-SHELL", "bash -c '(exec 3<>/dev/tcp/127.0.0.1/9000)' 2>/dev/null"]
interval: 5s
timeout: 3s
retries: 20
start_period: 30s
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: