#!/usr/bin/env bash # Build and run admin on the VPS (idempotent). # Expects app sources in APP_DIR (CI rsync) and a filled APP_DIR/.env. set -euo pipefail APP_DIR="${APP_DIR:-/opt/ghabilee-admin}" SRC_DIR="${SRC_DIR:-$APP_DIR/src}" IMAGE_TAG="${IMAGE_TAG:-ghabilee-admin:local}" cd "$APP_DIR" if [ ! -f "$APP_DIR/.env" ]; then echo "Missing $APP_DIR/.env — copy from .env.example and fill production values." >&2 exit 2 fi if [ ! -f "$SRC_DIR/Dockerfile" ]; then echo "Missing $SRC_DIR/Dockerfile — rsync the repo before deploy." >&2 exit 2 fi install -m 0644 "$SRC_DIR/deploy/docker-compose.production.yml" "$APP_DIR/docker-compose.yml" if [ -f "$SRC_DIR/deploy/nginx/backoffice.conf" ]; then if [ -s /etc/letsencrypt/live/backoffice.ghabilee.ir/fullchain.pem ]; then install -m 0644 "$SRC_DIR/deploy/nginx/backoffice.conf" /etc/nginx/sites-available/backoffice elif [ -f "$SRC_DIR/deploy/nginx/backoffice-http-bootstrap.conf" ]; then install -m 0644 "$SRC_DIR/deploy/nginx/backoffice-http-bootstrap.conf" /etc/nginx/sites-available/backoffice else install -m 0644 "$SRC_DIR/deploy/nginx/backoffice.conf" /etc/nginx/sites-available/backoffice fi ln -sfn /etc/nginx/sites-available/backoffice /etc/nginx/sites-enabled/backoffice nginx -t systemctl reload nginx fi # shellcheck disable=SC1091 set -a # shellcheck disable=SC1090 source "$APP_DIR/.env" set +a env_or_empty() { local key="$1" printf '%s' "${!key-}" } sanitize_api_proxy_target() { local target target="$(env_or_empty API_PROXY_TARGET)" case "$target" in *127.0.0.1*|*localhost*|*'::1'*) echo "Warning: refusing loopback API_PROXY_TARGET for production image build" >&2 printf '' ;; *) printf '%s' "$target" ;; esac } BUILD_ARGS=( --build-arg "NEXT_PUBLIC_API_URL=$(env_or_empty NEXT_PUBLIC_API_URL)" --build-arg "NEXT_PUBLIC_FILE_SERVER_URL=$(env_or_empty NEXT_PUBLIC_FILE_SERVER_URL)" --build-arg "MAP_API_KEY=$(env_or_empty MAP_API_KEY)" --build-arg "NEXT_PUBLIC_MAP_API_KEY=$(env_or_empty NEXT_PUBLIC_MAP_API_KEY)" --build-arg "NEXT_PUBLIC_VAPID_PUBLIC_KEY=$(env_or_empty NEXT_PUBLIC_VAPID_PUBLIC_KEY)" --build-arg "NEXT_PUBLIC_BASE_PATH=$(env_or_empty NEXT_PUBLIC_BASE_PATH)" --build-arg "NEXT_PUBLIC_SITE_URL=$(env_or_empty NEXT_PUBLIC_SITE_URL)" --build-arg "NEXT_PUBLIC_OBSERVABILITY_ENDPOINT=$(env_or_empty NEXT_PUBLIC_OBSERVABILITY_ENDPOINT)" --build-arg "NEXT_PUBLIC_SENTRY_DSN=$(env_or_empty NEXT_PUBLIC_SENTRY_DSN)" --build-arg "NEXT_PUBLIC_SENTRY_ENVIRONMENT=$(env_or_empty NEXT_PUBLIC_SENTRY_ENVIRONMENT)" --build-arg "NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$(env_or_empty NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE)" --build-arg "NEXT_PUBLIC_ARCAPTCHA_SITE_KEY=$(env_or_empty NEXT_PUBLIC_ARCAPTCHA_SITE_KEY)" --build-arg "SENTRY_AUTH_TOKEN=$(env_or_empty SENTRY_AUTH_TOKEN)" --build-arg "SENTRY_ORG=$(env_or_empty SENTRY_ORG)" --build-arg "SENTRY_PROJECT=$(env_or_empty SENTRY_PROJECT)" --build-arg "API_PROXY_TARGET=$(sanitize_api_proxy_target)" ) echo "Building ${IMAGE_TAG} on VPS (build arguments redacted)" docker build \ --platform linux/amd64 \ -t "$IMAGE_TAG" \ "${BUILD_ARGS[@]}" \ "$SRC_DIR" ADMIN_IMAGE="$IMAGE_TAG" docker compose -f "$APP_DIR/docker-compose.yml" up -d --no-deps ghabilee-admin for attempt in $(seq 1 36); do health="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' ghabilee-admin 2>/dev/null || echo missing)" echo "Admin health ${attempt}/36: ${health}" if [ "$health" = "healthy" ]; then # فقط tagهای قدیمی admin؛ prune -af کش/لایه‌های قابل‌استفاده مجدد را پاک می‌کند docker images --format '{{.Repository}}:{{.Tag}}' \ | awk -v keep="$IMAGE_TAG" '$0 ~ /^ghabilee-admin:/ && $0 != keep { print }' \ | while IFS= read -r old; do docker rmi "$old" >/dev/null 2>&1 || true done docker image prune -f >/dev/null 2>&1 || true echo "Deploy OK" exit 0 fi case "$health" in unhealthy|exited|dead|missing) exit 1 ;; esac sleep 5 done echo "Timed out waiting for healthy admin container" >&2 exit 1