Files
gustavoo-me/docker/wp-init.sh
T
Gustavo 4d666d1778 wp-init: provision fluent plugins via WP_EXTRA_PLUGINS
gustavoo-portfolio-core refuses to activate without fluentform, fluent-crm
and fluent-smtp, but those were gitignored and not installed by the setup:
any fresh clone ended with the core plugin inactive and the seeder skipped.
They are now part of the default WP_EXTRA_PLUGINS (verified slugs on
WordPress.org), with .env.example, compose default, README and .gitignore
updated to match.
2026-11-02 14:11:50 -03:00

417 lines
18 KiB
Bash
Executable File

#!/bin/sh
set -e
# Two modes:
# install — the database has no WordPress yet: core, language, plugins,
# theme and the project seeder are set up from scratch.
# update — the site already exists: only the WordPress core and the
# installed plugins are updated. Content, options, widgets,
# users and manually configured settings are never touched.
#
# Whatever a plugin/theme adds on disk (uploads, new plugin folders, edited
# files) survives, because state lives in the db_data and wordpress_data
# volumes, not in this script.
WP_PATH=/var/www/html
SITE_URL="${WORDPRESS_SITE_URL:-http://localhost:${WORDPRESS_PORT:-8080}}"
ADMIN_USER="${WORDPRESS_ADMIN_USER:-admin}"
ADMIN_PASSWORD="${WORDPRESS_ADMIN_PASSWORD:-admin}"
ADMIN_EMAIL="${WORDPRESS_ADMIN_EMAIL:-admin@example.com}"
ADMIN_TITLE="${WORDPRESS_SITE_TITLE:-Gustavo Portfolio}"
WP_THEME="${WP_THEME:-gustavoo-portfolio}"
WP_LOCALE="${WP_LOCALE:-pt_BR}"
# Plugins from the WordPress.org directory that must exist even when they are
# not committed to wp-content/plugins. Installed only when missing, then
# activated. Format: slug (or URL).
# fluentform, fluent-crm and fluent-smtp are required by gustavoo-portfolio-core
# (contact forms, CRM feeds, SMTP); without them the core plugin refuses to
# activate and the seeder never runs.
WP_EXTRA_PLUGINS="${WP_EXTRA_PLUGINS:-post-views-counter seo-by-rank-math google-site-kit fluentform fluent-crm fluent-smtp}"
wp_run() {
wp --path="$WP_PATH" "$@"
}
log() {
echo "[wpcli] $*"
}
warn() {
echo "[wpcli] WARN: $*" >&2
}
# ─── Wait for dependencies ───────────────────────────────────────────────────
# The loop fails fast on wrong credentials instead of spinning forever: when
# the password in .env does not match the one stored in the db_data volume,
# `db check` answers "Access denied" immediately, and waiting longer changes
# nothing. Any other failure (server down, still starting) keeps waiting, with
# a timeout so a dead database cannot hang the boot silently.
log "Waiting for database..."
DB_TRIES=0
while true; do
if DB_WAIT_OUT="$(wp_run db check 2>&1)"; then
break
fi
if printf '%s' "$DB_WAIT_OUT" | grep -q "Access denied"; then
echo "[wpcli] ERROR: the database rejected the credentials from .env." >&2
echo "[wpcli]" >&2
echo "[wpcli] The database volume keeps the password used on the first run." >&2
echo "[wpcli] Changing WORDPRESS_DB_PASSWORD or MARIADB_ROOT_PASSWORD in" >&2
echo "[wpcli] .env alone does not update it, so WordPress stops connecting." >&2
echo "[wpcli]" >&2
echo "[wpcli] Put the original password back in .env and restart the containers." >&2
echo "[wpcli] (To really change it, see 'Passwords' in README.md.)" >&2
exit 1
fi
DB_TRIES=$((DB_TRIES + 1))
if [ "$DB_TRIES" -ge 150 ]; then
echo "[wpcli] ERROR: the database did not become ready in time." >&2
printf '%s\n' "$DB_WAIT_OUT" | tail -3 >&2
exit 1
fi
sleep 2
done
log "Waiting for WordPress core files..."
until [ -f "$WP_PATH/wp-load.php" ]; do
sleep 2
done
# The wp:cli image runs as a different uid than the wordpress image. Grant
# write access to whoever needs it without changing the volume ownership:
# the core folder (plugins, themes, wp-config.php) and wp-content itself.
# Bind-mounted wp-content/plugins|themes come from the host, so their
# permissions are checked too.
if [ ! -w "$WP_PATH/wp-content" ]; then
log "$WP_PATH/wp-content not writable by uid $(id -u), fixing permissions..."
find "$WP_PATH" -maxdepth 1 -exec chmod g+rwX {} + || true
find "$WP_PATH/wp-content" -maxdepth 1 -exec chmod g+rwX {} + || true
mkdir -p "$WP_PATH/wp-content/uploads" 2>/dev/null || true
find "$WP_PATH/wp-content/uploads" -maxdepth 1 -exec chmod g+rwX {} + 2>/dev/null || true
fi
# Plugins are installed by wp-cli inside the bind-mounted folder, so wp-cli
# must be able to write there. The compose default (user 33:33) matches the
# wordpress image; a host-owned folder is reported instead of failing silently.
for dir in "$WP_PATH/wp-content/plugins" "$WP_PATH/wp-content/themes"; do
[ -d "$dir" ] || continue
if [ ! -w "$dir" ]; then
log "$dir is owned by another user — adjusting group permissions..."
find "$dir" -maxdepth 0 -exec chmod g+rwX {} + 2>/dev/null || true
fi
if [ ! -w "$dir" ]; then
warn "cannot write to $dir as uid $(id -u)/gid $(id -g). Plugin install/update will fail."
warn "fix with: sudo chmod -R g+rwX wp-content/plugins wp-content/themes && sudo chgrp $(id -gn) wp-content/plugins wp-content/themes"
fi
done
if [ ! -w "$WP_PATH" ]; then
echo "[wpcli] ERROR: $WP_PATH is not writable by uid $(id -u). Aborting." >&2
exit 1
fi
# ─── Mode detection ──────────────────────────────────────────────────────────
if wp_run core is-installed 2>/dev/null; then
MODE=update
else
MODE=install
fi
# ═════════════════════════════════════════════════════════════════════════════
# INSTALL — first run on an empty database
# ═════════════════════════════════════════════════════════════════════════════
if [ "$MODE" = install ]; then
log "No WordPress installation found — starting first-time setup."
log "Installing WordPress core at $SITE_URL..."
wp_run core install \
--url="$SITE_URL" \
--title="$ADMIN_TITLE" \
--admin_user="$ADMIN_USER" \
--admin_password="$ADMIN_PASSWORD" \
--admin_email="$ADMIN_EMAIL" \
--skip-email
log "Setting site URL to $SITE_URL..."
wp_run option update home "$SITE_URL"
wp_run option update siteurl "$SITE_URL"
# ═════════════════════════════════════════════════════════════════════════════
# UPDATE — the site already exists, keep every piece of data
# ═════════════════════════════════════════════════════════════════════════════
else
log "Existing installation detected — keeping database, content and settings."
fi
# ─── Language (both modes) ───────────────────────────────────────────────────
if ! wp_run language core is-installed "$WP_LOCALE" 2>/dev/null; then
log "Installing $WP_LOCALE language pack..."
wp_run language core install "$WP_LOCALE"
fi
if [ "$(wp_run option get WPLANG 2>/dev/null || true)" != "$WP_LOCALE" ]; then
log "Activating $WP_LOCALE site language..."
wp_run language core activate "$WP_LOCALE"
fi
# ─── Redis constants (both modes) ────────────────────────────────────────────
# Values from .env override wp-config.php; anything empty keeps the current
# value, so a manual edit is never lost.
REDIS_HOST="${WP_REDIS_HOST:-}"
REDIS_PORT="${WP_REDIS_PORT:-}"
REDIS_PASSWORD="${WP_REDIS_PASSWORD:-}"
REDIS_DATABASE="${WP_REDIS_DATABASE:-}"
if [ -n "$REDIS_HOST" ]; then
wp_run config set WP_REDIS_HOST "$REDIS_HOST" --type=constant >/dev/null
fi
if [ -n "$REDIS_PORT" ]; then
wp_run config set WP_REDIS_PORT "$REDIS_PORT" --type=constant --raw >/dev/null
fi
if [ -n "$REDIS_PASSWORD" ]; then
wp_run config set WP_REDIS_PASSWORD "$REDIS_PASSWORD" --type=constant >/dev/null
fi
if [ -n "$REDIS_DATABASE" ]; then
wp_run config set WP_REDIS_DATABASE "$REDIS_DATABASE" --type=constant --raw >/dev/null
fi
# ═════════════════════════════════════════════════════════════════════════════
# UPDATES — only on an existing installation, only when an update exists.
# Uses the WordPress upgrader so database migrations run as on the dashboard.
# ═════════════════════════════════════════════════════════════════════════════
update_core() {
UPDATE_JSON="$(wp_run core check-update --format=json 2>/dev/null || echo '[]')"
case "$UPDATE_JSON" in
''|'[]'|'null')
log "WordPress core is up to date ($(wp_run core version 2>/dev/null))."
return 0
;;
esac
# no --minor: security/latest release is the desired behaviour, and the
# major upgrade keeps content and database intact through wp-cli.
log "New WordPress version available — updating core..."
wp_run core update || warn "core update failed"
}
update_plugins() {
# Only plugins that exist locally are updated; nothing new is downloaded.
STALE="$(wp_run plugin list --status=active --fields=name,update --format=csv 2>/dev/null \
| awk -F, 'NR>1 && $2 != "" && $2 != "none" && $2 != "available" {print $1}')"
if [ -z "$STALE" ]; then
log "All active plugins are up to date."
return 0
fi
for plugin in $STALE; do
log "Updating plugin '$plugin'..."
wp_run plugin update "$plugin" || warn "could not update plugin '$plugin'"
done
}
if [ "$MODE" = update ] && [ -z "${WP_SKIP_UPDATES:-}" ]; then
update_core
update_plugins
fi
# ─── Extra plugins from WordPress.org (both modes) ───────────────────────────
# These plugins are not versioned in Git: the setup installs them from the
# WordPress.org directory so the repository stays small. Only installed when
# missing: an update is handled by the update step, so a local version or
# manual change is never overwritten here.
INSTALLED_ANY=0
FAILED_PLUGINS=""
for plugin in $WP_EXTRA_PLUGINS; do
if wp_run plugin is-installed "$plugin" 2>/dev/null; then
continue
fi
log "Installing plugin '$plugin' from WordPress.org..."
if wp_run plugin install "$plugin"; then
INSTALLED_ANY=1
else
warn "could not install plugin '$plugin'"
FAILED_PLUGINS="$FAILED_PLUGINS $plugin"
fi
done
# ─── Plugins (both modes) ────────────────────────────────────────────────────
# Every plugin in the bind-mounted wp-content/plugins folder is the desired set:
# new folders are activated, missing ones are never downloaded. Plugins bundled
# with WordPress that are not part of the project are removed.
FORBIDDEN_PATTERN="${FORBIDDEN_PATTERN:-^akismet$|^hello$}"
if [ -n "${WP_ALLOW_FORBIDDEN_PLUGINS:-}" ]; then
FORBIDDEN_PATTERN=""
fi
# Drop-ins (db.php, object-cache.php, ...) live in wp-content and are managed
# by their parent plugin, they are never activated directly.
is_dropin() {
wp_run plugin list --field=name --status=dropin 2>/dev/null | grep -qx "$1"
}
log "Activating plugins found in $WP_PATH/wp-content/plugins..."
for plugin in $(wp_run plugin list --field=name --skip-plugins --skip-themes 2>/dev/null || wp_run plugin list --field=name); do
if [ -n "$FORBIDDEN_PATTERN" ] && printf '%s' "$plugin" | grep -Eq "$FORBIDDEN_PATTERN"; then
continue
fi
if is_dropin "$plugin"; then
continue
fi
if ! wp_run plugin is-active "$plugin" 2>/dev/null; then
log "Activating plugin '$plugin'..."
wp_run plugin activate "$plugin" || warn "could not activate '$plugin'"
fi
done
for plugin in $(wp_run plugin list --field=name --skip-plugins --skip-themes 2>/dev/null || wp_run plugin list --field=name); do
if [ -z "$FORBIDDEN_PATTERN" ]; then
break
fi
if printf '%s' "$plugin" | grep -Eq "$FORBIDDEN_PATTERN"; then
log "Removing unwanted plugin '$plugin'..."
wp_run plugin deactivate "$plugin" 2>/dev/null || true
wp_run plugin uninstall "$plugin" 2>/dev/null || true
wp_run plugin delete "$plugin" 2>/dev/null || true
fi
done
# ─── Post Views Counter (first install only) ─────────────────────────────────
# Counts views for posts and pages, with manual placement so the theme decides
# where the counter appears (shortcode [post-views]). Existing settings are
# never changed: this only runs on a fresh installation.
if [ "$MODE" = install ] && wp_run plugin is-active post-views-counter 2>/dev/null; then
log "Configuring Post Views Counter (posts + pages, manual placement)..."
wp_run eval '
$general = get_option( "post_views_counter_settings_general", array() );
if ( ! is_array( $general ) ) {
$general = array();
}
$types = isset( $general["post_types_count"] ) && is_array( $general["post_types_count"] )
? $general["post_types_count"]
: array( "post" );
foreach ( array( "post", "page" ) as $type ) {
if ( ! in_array( $type, $types, true ) ) {
$types[] = $type;
}
}
$general["post_types_count"] = array_values( $types );
update_option( "post_views_counter_settings_general", $general );
$display = get_option( "post_views_counter_settings_display", array() );
if ( ! is_array( $display ) ) {
$display = array();
}
$display["position"] = "manual";
update_option( "post_views_counter_settings_display", $display );
$pvc = Post_Views_Counter();
if ( isset( $pvc->options ) ) {
$pvc->options["general"]["post_types_count"] = $general["post_types_count"];
$pvc->options["display"]["position"] = "manual";
}
echo "Post Views Counter: post_types=[post,page] position=[manual]" . PHP_EOL;
' || warn "could not configure Post Views Counter"
fi
# Rank Math SEO and Site Kit are installed and activated only; no wizard, SEO
# settings, Search Console or Analytics connection is configured here.
# ─── Redis object cache (both modes) ─────────────────────────────────────────
if ! wp_run plugin is-installed redis-cache 2>/dev/null; then
log "Installing Redis Object Cache plugin..."
wp_run plugin install redis-cache
fi
if ! wp_run plugin is-active redis-cache 2>/dev/null; then
log "Activating Redis Object Cache plugin..."
wp_run plugin activate redis-cache || warn "could not activate redis-cache"
fi
# The drop-in (wp-content/object-cache.php) is what actually routes the cache to
# Redis; it is (re)created when missing, stale or disabled.
if ! wp_run redis status 2>/dev/null | grep -qE '^[[:space:]]*Drop-in:[[:space:]]+Valid'; then
log "Enabling Redis object cache drop-in..."
wp_run redis enable >/dev/null 2>&1 || wp_run redis update-dropin >/dev/null 2>&1 || true
fi
if [ -n "$REDIS_HOST" ]; then
REDIS_STATUS="$(wp_run redis status 2>/dev/null || true)"
# The status output may lead with the "Drop-ins: [...]" block, so the checks
# match the key line anywhere in the output.
if printf '%s' "$REDIS_STATUS" | grep -qE '^[[:space:]]*Status:[[:space:]]+Connected'; then
log "Redis object cache connected."
else
warn "Redis object cache could not connect — check the redis service."
fi
if printf '%s' "$REDIS_STATUS" | grep -qE '^[[:space:]]*Drop-in:[[:space:]]+Valid'; then
log "Redis drop-in (wp-content/object-cache.php) is valid."
else
warn "Redis drop-in is missing or invalid — re-run 'docker compose restart wpcli'."
fi
fi
# ─── Theme (both modes) ──────────────────────────────────────────────────────
# Checked on disk, not with `wp theme is-installed`: in an existing install
# that check needs the database, and any hiccup made the script skip the theme.
if [ ! -f "$WP_PATH/wp-content/themes/$WP_THEME/style.css" ]; then
echo "[wpcli] ERROR: theme '$WP_THEME' is missing from wp-content/themes." >&2
echo "[wpcli] It is part of the repository: run 'git checkout -- wp-content/themes'" >&2
echo "[wpcli] on the host, then restart the containers." >&2
exit 1
fi
# Always attempted (harmless when already active) and fatal on failure: a site
# without its theme renders broken pages while the script reports success.
log "Activating $WP_THEME theme..."
if ! wp_run theme activate "$WP_THEME"; then
echo "[wpcli] ERROR: could not activate theme '$WP_THEME'." >&2
exit 1
fi
# Only project themes stay available: every other theme (the WordPress defaults)
# is removed. WP_KEEP_THEMES lists themes to preserve on purpose.
log "Removing leftover themes..."
for theme in $(wp_run theme list --field=name 2>/dev/null); do
if [ "$theme" = "$WP_THEME" ]; then
continue
fi
if [ -n "${WP_KEEP_THEMES:-}" ] && printf '%s' " $WP_KEEP_THEMES " | grep -q " $theme "; then
continue
fi
if wp_run theme is-active "$theme" 2>/dev/null; then
continue
fi
log "Removing theme '$theme'..."
wp_run theme delete "$theme" || warn "could not delete theme '$theme'"
done
# ─── Project seeder (both modes, idempotent) ─────────────────────────────────
# Only fills in what is missing (pages, forms, feed, widget defaults). Content
# edited by the administrator is never overwritten.
if wp_run plugin is-active gustavoo-portfolio-core 2>/dev/null; then
log "Running portfolio seeder..."
wp_run eval 'GSO_Seeder::maybe_seed();' || warn "seeder failed"
log "Seeder state: $(wp_run option get gso_portfolio_seed_state --format=json 2>/dev/null || echo '{}')"
fi
log "Setup complete ($MODE). Site available at $SITE_URL"
# A plugin that could not be installed leaves the site without a feature the
# project expects (SEO, analytics, view counts). Fail loudly instead of
# reporting success: the container is restarted by the operator and the next
# run simply tries again, since installation is idempotent.
if [ -n "$FAILED_PLUGINS" ]; then
warn "Required plugins are missing:$FAILED_PLUGINS"
warn "Check network access to downloads.wordpress.org and restart the container."
exit 1
fi