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.
417 lines
18 KiB
Bash
Executable File
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
|