Run wp-cli as www-data instead of root; drop --allow-root

--allow-root was routing around wp-cli's own safety check rather than
addressing why root was there in the first place: docker exec defaults to
the container's root user because the image never sets a non-root user
for exec sessions — it was never about the actual web-facing attack
surface, which already runs as www-data (verified: php-fpm's worker
processes, the ones executing plugin/theme code for real requests, run as
uid 33, not root; only our own deliberate admin commands were root).

wp-config.php and the rest of wp-core are already owned by www-data (the
official entrypoint sets this up), so there's no actual reason for our
own commands to run as anything else. Verified: a full clean deploy and
a bookstore-core wp-cli command both work identically running as
www-data, no functional change, just removed an unnecessary privilege.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 13:39:19 -04:00
co-authored by Claude Sonnet 5
parent a782f880bc
commit 6299391f23
4 changed files with 22 additions and 17 deletions
+3 -2
View File
@@ -22,12 +22,13 @@ ps:
logs:
$(COMPOSE) logs -f
# add -u root yourself for one-off root debugging (installing a package, etc.)
shell:
$(COMPOSE) exec wordpress bash
$(COMPOSE) exec -u www-data wordpress bash
# make wp ENV=staging ARGS="plugin list"
wp:
$(COMPOSE) exec wordpress wp $(ARGS) --allow-root
$(COMPOSE) exec -u www-data wordpress wp $(ARGS)
deploy:
./deploy/deploy.sh $(ENV)
+1 -1
View File
@@ -33,7 +33,7 @@ $COMPOSE exec -T db sh -c "exec mariadb-dump -u\"\$MARIADB_USER\" -p\"\$(cat /ru
| gzip > "$BACKUP_DIR/db-${TIMESTAMP}.sql.gz"
echo "==> archiving uploads"
$COMPOSE run --rm -T wordpress tar -czf - -C /var/www/html/wp-content uploads \
$COMPOSE run --rm -T -u www-data wordpress tar -czf - -C /var/www/html/wp-content uploads \
> "$BACKUP_DIR/uploads-${TIMESTAMP}.tar.gz"
if [[ -n "${BACKUP_REMOTE:-}" ]]; then
+17 -13
View File
@@ -44,6 +44,11 @@ grep -Ev '^(DB_PASSWORD|DB_ROOT_PASSWORD)=' "$ENV_FILE" > "$COMPOSE_ENV_FILE"
# 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}"
@@ -78,36 +83,35 @@ for i in $(seq 1 60); do
done
echo "==> ensuring WordPress is installed"
if ! $COMPOSE exec -T wordpress wp core is-installed --allow-root; then
$COMPOSE exec -T wordpress wp core install \
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 \
--allow-root
--skip-email
fi
echo "==> installing/activating theme (Blocksy — free; Book Store starter site needs a manual license + import, see README)"
$COMPOSE exec -T wordpress wp theme install blocksy --activate --allow-root
$WP theme install blocksy --activate
echo "==> installing/activating required plugins"
$COMPOSE exec -T wordpress wp plugin install woocommerce redis-cache blocksy-companion --activate --allow-root
$COMPOSE exec -T wordpress wp plugin activate bookstore-core --allow-root
$COMPOSE exec -T wordpress wp redis enable --allow-root || true
$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)"
$COMPOSE exec -T wordpress wp option update woocommerce_custom_orders_table_enabled yes --allow-root
$COMPOSE exec -T wordpress wp option update woocommerce_custom_orders_table_data_sync_enabled yes --allow-root
$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)"
$COMPOSE exec -T wordpress wp option update blog_public 0 --allow-root
$WP option update blog_public 0
fi
echo "==> flushing caches and rewrite rules"
$COMPOSE exec -T wordpress wp cache flush --allow-root
$COMPOSE exec -T wordpress wp rewrite flush --allow-root
$WP cache flush
$WP rewrite flush
echo "==> deploy complete: ${ENVIRONMENT}"
+1 -1
View File
@@ -31,7 +31,7 @@ gunzip -c "$DB_DUMP" | $COMPOSE exec -T db sh -c "exec mariadb -u\"\$MARIADB_USE
echo "==> restoring uploads"
ARCHIVE_DIR="$(cd "$(dirname "$UPLOADS_ARCHIVE")" && pwd)"
ARCHIVE_NAME="$(basename "$UPLOADS_ARCHIVE")"
$COMPOSE run --rm -T -v "${ARCHIVE_DIR}:/restore:ro" wordpress \
$COMPOSE run --rm -T -u www-data -v "${ARCHIVE_DIR}:/restore:ro" wordpress \
tar -xzf "/restore/${ARCHIVE_NAME}" -C /var/www/html/wp-content
echo "==> restore complete — verify the site before treating the drill as passed"