#!/usr/bin/env bash # Builds and brings up an environment, then makes sure WordPress/WooCommerce # are installed and configured to match design doc §01/§03/§07. Safe to run # repeatedly — every step is idempotent. set -euo pipefail ENVIRONMENT="${1:?Usage: deploy.sh }" export ENVIRONMENT REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" case "$ENVIRONMENT" in dev|staging|production) ;; *) echo "environment must be 'dev', 'staging', or 'production'" >&2; exit 1 ;; esac ENV_FILE=".env.${ENVIRONMENT}" if [[ ! -f "$ENV_FILE" ]]; then echo "missing $ENV_FILE — copy .env.example to $ENV_FILE and fill it in" >&2 exit 1 fi # shellcheck source=lib/env.sh source "$REPO_ROOT/deploy/lib/env.sh" SITE_URL="$(env_get "$ENV_FILE" SITE_URL)" SITE_TITLE="$(env_get "$ENV_FILE" SITE_TITLE)" WP_ADMIN_USER="$(env_get "$ENV_FILE" WP_ADMIN_USER)" WP_ADMIN_PASSWORD="$(env_get "$ENV_FILE" WP_ADMIN_PASSWORD)" WP_ADMIN_EMAIL="$(env_get "$ENV_FILE" WP_ADMIN_EMAIL)" # DB_PASSWORD/DB_ROOT_PASSWORD are stripped from what Compose itself loads: # Compose scans every value in --env-file for $identifier-looking patterns # as part of building its own interpolation table, and warns "variable not # set" for any it finds — even though nothing consumes these two via ${VAR} # anymore (they only flow through secrets/, via env_get below, which reads # the real, unfiltered $ENV_FILE directly). Filtering them out of Compose's # copy is what makes that warning actually go away, permanently, regardless # of what characters end up in either password. COMPOSE_ENV_FILE="$(mktemp)" trap 'rm -f "$COMPOSE_ENV_FILE"' EXIT grep -Ev '^(DB_PASSWORD|DB_ROOT_PASSWORD)=' "$ENV_FILE" > "$COMPOSE_ENV_FILE" # -p pins the Compose project name to the environment (default is the # directory name, which every environment shares — that made staging quietly # reuse dev's db_data volume/credentials the first time this ran). COMPOSE="docker compose -p bookstore-${ENVIRONMENT} -f docker-compose.yml -f docker-compose.${ENVIRONMENT}.yml --env-file ${COMPOSE_ENV_FILE}" # wp-cli runs as www-data (who already owns wp-config.php etc.), not root — # `docker exec` defaults to root only because the image never sets a # non-root default user for exec sessions; the actual web-facing php-fpm # workers already run as www-data regardless. WP="$COMPOSE exec -T -u www-data wordpress wp" echo "==> writing DB secret files (bypasses compose's \${VAR} interpolation entirely)" SECRETS_DIR="secrets/${ENVIRONMENT}" mkdir -p "$SECRETS_DIR" chmod 700 "secrets" "$SECRETS_DIR" 2>/dev/null || true DB_PASSWORD_VALUE="$(env_get "$ENV_FILE" DB_PASSWORD)" DB_ROOT_PASSWORD_VALUE="$(env_get "$ENV_FILE" DB_ROOT_PASSWORD)" : "${DB_PASSWORD_VALUE:?set DB_PASSWORD in ${ENV_FILE}}" : "${DB_ROOT_PASSWORD_VALUE:?set DB_ROOT_PASSWORD in ${ENV_FILE}}" printf '%s' "$DB_PASSWORD_VALUE" > "$SECRETS_DIR/db_password" printf '%s' "$DB_ROOT_PASSWORD_VALUE" > "$SECRETS_DIR/db_root_password" # 644, not 600: the db/wordpress/cron containers read this as their own # (non-host-matching) container UID, e.g. www-data — chmod 600 made it # unreadable to them. The containing directory (700, above) is what # actually keeps other host users out; these just need to be world-readable # within that already-restricted directory. chmod 644 "$SECRETS_DIR/db_password" "$SECRETS_DIR/db_root_password" echo "==> building and starting ${ENVIRONMENT}" $COMPOSE up -d --build echo "==> waiting for WordPress core files (the official image copies them in on first boot of an empty volume, which takes a few seconds)" for i in $(seq 1 60); do if $COMPOSE exec -T wordpress test -f /var/www/html/wp-settings.php >/dev/null 2>&1; then break fi if [[ "$i" -eq 60 ]]; then echo "wordpress core files did not appear in time" >&2 exit 1 fi sleep 2 done echo "==> ensuring WordPress is installed" if ! $WP core is-installed; then $WP core install \ --url="${SITE_URL:?set SITE_URL in ${ENV_FILE}}" \ --title="${SITE_TITLE:-Bookstore}" \ --admin_user="${WP_ADMIN_USER:?set WP_ADMIN_USER in ${ENV_FILE}}" \ --admin_password="${WP_ADMIN_PASSWORD:?set WP_ADMIN_PASSWORD in ${ENV_FILE}}" \ --admin_email="${WP_ADMIN_EMAIL:?set WP_ADMIN_EMAIL in ${ENV_FILE}}" \ --skip-email fi echo "==> installing/activating theme (Blocksy — free; Book Store starter site needs a manual license + import, see README)" $WP theme install blocksy --activate echo "==> installing/activating required plugins" $WP plugin install woocommerce redis-cache blocksy-companion --activate $WP plugin activate bookstore-core $WP redis enable || true echo "==> enabling WooCommerce HPOS (custom order tables)" $WP option update woocommerce_custom_orders_table_enabled yes $WP option update woocommerce_custom_orders_table_data_sync_enabled yes if [[ "$ENVIRONMENT" != "production" ]]; then echo "==> discouraging search engines (non-production)" $WP option update blog_public 0 fi echo "==> flushing caches and rewrite rules" $WP cache flush $WP rewrite flush echo "==> deploy complete: ${ENVIRONMENT}"