diff --git a/Makefile b/Makefile index 6f66d09..eff0f53 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,12 @@ ENV ?= dev export ENVIRONMENT = $(ENV) -COMPOSE = docker compose -p bookstore-$(ENV) -f docker-compose.yml -f docker-compose.$(ENV).yml --env-file .env.$(ENV) +# DB_PASSWORD/DB_ROOT_PASSWORD are stripped from what Compose loads — they +# only flow through secrets/ now, and Compose warns "variable not set" on +# any $-shaped value in --env-file even when nothing consumes it. Rewritten +# fresh on every `make` invocation, so it can't drift from .env.$(ENV). +COMPOSE_ENV_FILE := .env.$(ENV).compose +$(shell grep -Ev '^(DB_PASSWORD|DB_ROOT_PASSWORD)=' .env.$(ENV) > $(COMPOSE_ENV_FILE) 2>/dev/null) +COMPOSE = docker compose -p bookstore-$(ENV) -f docker-compose.yml -f docker-compose.$(ENV).yml --env-file $(COMPOSE_ENV_FILE) .PHONY: up down ps logs shell wp deploy backup diff --git a/deploy/backup.sh b/deploy/backup.sh index 5a8cab8..0cc305f 100755 --- a/deploy/backup.sh +++ b/deploy/backup.sh @@ -19,7 +19,14 @@ BACKUP_RETENTION_DAYS="$(env_get "$ENV_FILE" BACKUP_RETENTION_DAYS)" BACKUP_DIR="${BACKUP_DIR:-./backups}/${ENVIRONMENT}" mkdir -p "$BACKUP_DIR" -COMPOSE="docker compose -p bookstore-${ENVIRONMENT} -f docker-compose.yml -f docker-compose.${ENVIRONMENT}.yml --env-file ${ENV_FILE}" +# See deploy.sh for why: Compose warns on any $-shaped value in --env-file +# even when unused, so DB_PASSWORD/DB_ROOT_PASSWORD are filtered out of the +# copy Compose actually sees. +COMPOSE_ENV_FILE="$(mktemp)" +trap 'rm -f "$COMPOSE_ENV_FILE"' EXIT +grep -Ev '^(DB_PASSWORD|DB_ROOT_PASSWORD)=' "$ENV_FILE" > "$COMPOSE_ENV_FILE" + +COMPOSE="docker compose -p bookstore-${ENVIRONMENT} -f docker-compose.yml -f docker-compose.${ENVIRONMENT}.yml --env-file ${COMPOSE_ENV_FILE}" echo "==> dumping database" $COMPOSE exec -T db sh -c "exec mariadb-dump -u\"\$MARIADB_USER\" -p\"\$(cat /run/secrets/db_password)\" \"\$MARIADB_DATABASE\"" \ diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 5f10673..0405aa3 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -28,10 +28,22 @@ 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 ${ENV_FILE}" +COMPOSE="docker compose -p bookstore-${ENVIRONMENT} -f docker-compose.yml -f docker-compose.${ENVIRONMENT}.yml --env-file ${COMPOSE_ENV_FILE}" echo "==> writing DB secret files (bypasses compose's \${VAR} interpolation entirely)" SECRETS_DIR="secrets/${ENVIRONMENT}" diff --git a/deploy/restore.sh b/deploy/restore.sh index d454372..e50dc27 100755 --- a/deploy/restore.sh +++ b/deploy/restore.sh @@ -12,7 +12,14 @@ cd "$REPO_ROOT" ENV_FILE=".env.staging" export ENVIRONMENT=staging -COMPOSE="docker compose -p bookstore-staging -f docker-compose.yml -f docker-compose.staging.yml --env-file ${ENV_FILE}" +# See deploy.sh for why: Compose warns on any $-shaped value in --env-file +# even when unused, so DB_PASSWORD/DB_ROOT_PASSWORD are filtered out of the +# copy Compose actually sees. +COMPOSE_ENV_FILE="$(mktemp)" +trap 'rm -f "$COMPOSE_ENV_FILE"' EXIT +grep -Ev '^(DB_PASSWORD|DB_ROOT_PASSWORD)=' "$ENV_FILE" > "$COMPOSE_ENV_FILE" + +COMPOSE="docker compose -p bookstore-staging -f docker-compose.yml -f docker-compose.staging.yml --env-file ${COMPOSE_ENV_FILE}" echo "!! this OVERWRITES the staging database and uploads !!" read -r -p "type 'restore' to continue: " confirm diff --git a/docker-compose.production.yml b/docker-compose.production.yml index 072b038..781bdd4 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -16,8 +16,8 @@ services: ports: - "80:80" - "443:443" - env_file: - - .env.production + environment: + SITE_DOMAIN: ${SITE_DOMAIN} volumes: - ./docker/caddy/Caddyfile.production:/etc/caddy/Caddyfile:ro - wp_core:/var/www/html:ro diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 2ae91fa..525a8b0 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -16,8 +16,8 @@ services: ports: - "80:80" - "443:443" - env_file: - - .env.staging + environment: + SITE_DOMAIN: ${SITE_DOMAIN} volumes: - ./docker/caddy/Caddyfile.staging:/etc/caddy/Caddyfile:ro - wp_core:/var/www/html:ro diff --git a/docker-compose.yml b/docker-compose.yml index 93537a6..71495a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,10 +12,20 @@ # 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`. The supplier/payment keys below are -# still passed the old 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. +# .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} @@ -38,15 +48,6 @@ x-bookstore-env: &bookstore-env HELCIM_ACCOUNT_ID: ${HELCIM_ACCOUNT_ID:-} MAILERLITE_API_KEY: ${MAILERLITE_API_KEY:-} -x-bookstore-secrets: &bookstore-db-secret - - db_password - -secrets: - db_password: - file: ./secrets/${ENVIRONMENT}/db_password - db_root_password: - file: ./secrets/${ENVIRONMENT}/db_root_password - services: db: image: mariadb:11 @@ -56,11 +57,10 @@ services: MARIADB_USER: ${DB_USER} MARIADB_PASSWORD_FILE: /run/secrets/db_password MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password - secrets: - - db_password - - 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 @@ -88,13 +88,13 @@ services: redis: condition: service_healthy environment: *bookstore-env - secrets: *bookstore-db-secret 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: @@ -104,7 +104,6 @@ services: - wordpress entrypoint: ["/bin/sh", "/usr/local/bin/cron-entrypoint.sh"] environment: *bookstore-env - secrets: *bookstore-db-secret volumes: - wp_core:/var/www/html - wp_uploads:/var/www/html/wp-content/uploads @@ -112,6 +111,7 @@ services: - ./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: diff --git a/docker/caddy/Caddyfile.staging b/docker/caddy/Caddyfile.staging index b50a21e..132c060 100644 --- a/docker/caddy/Caddyfile.staging +++ b/docker/caddy/Caddyfile.staging @@ -1,4 +1,4 @@ -{$SITE_DOMAIN} { +http://{$SITE_DOMAIN} { encode gzip # Launch gate: "No staging URLs are publicly indexed." Belt-and-suspenders