# 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. 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 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), so when that code is written it should # read `_FILE` variants the same way. 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: ${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}/db_password:/run/secrets/db_password:ro - ./secrets/${ENVIRONMENT}/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}/db_password:/run/secrets/db_password:ro 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 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}/db_password:/run/secrets/db_password:ro volumes: db_data: redis_data: wp_core: wp_uploads: wp_themes: