#!/usr/bin/env bash # 🐳 Build production Docker images for Plesk deployment # Features: Parallel builds, BuildKit, compressed tarballs set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" # Configuration (override via env vars or flags) IMAGE_FRONTEND="${IMAGE_FRONTEND_NAME:-portal-frontend}" IMAGE_BACKEND="${IMAGE_BACKEND_NAME:-portal-backend}" IMAGE_TAG="${IMAGE_TAG:-}" OUTPUT_DIR="${OUTPUT_DIR:-$PROJECT_ROOT}" PUSH_REMOTE="${PUSH_REMOTE:-}" PARALLEL="${PARALLEL_BUILD:-1}" COMPRESS="${COMPRESS:-1}" USE_LATEST_FILENAME="${USE_LATEST_FILENAME:-1}" # Default: save as .latest.tar.gz SAVE_TARS=1 # Colors G='\033[0;32m' Y='\033[1;33m' R='\033[0;31m' B='\033[0;34m' N='\033[0m' log() { echo -e "${G}[BUILD]${N} $*"; } info() { echo -e "${B}[BUILD]${N} $*"; } warn() { echo -e "${Y}[BUILD]${N} $*"; } fail() { echo -e "${R}[BUILD] ERROR:${N} $*"; exit 1; } usage() { cat < Version tag for image (default: YYYYMMDD-gitsha) --output Output directory (default: project root) --push Push to registry after build --no-save Build only, no tar files --no-compress Save as .tar instead of .tar.gz --versioned Name files with version tag (default: .latest.tar.gz) --sequential Build one at a time (default: parallel) -h, --help Show this help Examples: $0 # Output: portal-frontend.latest.tar.gz (default) $0 --versioned # Output: portal-frontend.20251201-abc123.tar.gz $0 --tag v1.2.3 --versioned # Output: portal-frontend.v1.2.3.tar.gz $0 --sequential --no-save # Debug build EOF exit 0 } # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in --tag) IMAGE_TAG="${2:-}"; shift 2 ;; --output) OUTPUT_DIR="${2:-}"; shift 2 ;; --push) PUSH_REMOTE="${2:-}"; shift 2 ;; --no-save) SAVE_TARS=0; shift ;; --no-compress) COMPRESS=0; shift ;; --versioned) USE_LATEST_FILENAME=0; shift ;; --sequential) PARALLEL=0; shift ;; -h|--help) usage ;; *) fail "Unknown option: $1" ;; esac done # Validation command -v docker >/dev/null 2>&1 || fail "Docker required" cd "$PROJECT_ROOT" [[ -f apps/portal/Dockerfile ]] || fail "Missing apps/portal/Dockerfile" [[ -f apps/bff/Dockerfile ]] || fail "Missing apps/bff/Dockerfile" # Auto-generate tag if not provided [[ -z "$IMAGE_TAG" ]] && IMAGE_TAG="$(date +%Y%m%d)-$(git rev-parse --short HEAD 2>/dev/null || echo 'local')" # Enable BuildKit export DOCKER_BUILDKIT=1 # Build args (can be overridden via env vars) PNPM_VERSION="${PNPM_VERSION:-10.15.0}" NEXT_PUBLIC_API_BASE="${NEXT_PUBLIC_API_BASE:-/api}" NEXT_PUBLIC_APP_NAME="${NEXT_PUBLIC_APP_NAME:-Customer Portal}" GIT_SOURCE="$(git config --get remote.origin.url 2>/dev/null || echo unknown)" log "🏷️ Tag: ${IMAGE_TAG}" LOG_DIR="${OUTPUT_DIR}/.build-logs" mkdir -p "$LOG_DIR" build_frontend() { local logfile="$LOG_DIR/frontend.log" docker build -f apps/portal/Dockerfile \ --build-arg "PNPM_VERSION=${PNPM_VERSION}" \ --build-arg "NEXT_PUBLIC_API_BASE=${NEXT_PUBLIC_API_BASE}" \ --build-arg "NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME}" \ --build-arg "NEXT_PUBLIC_APP_VERSION=${IMAGE_TAG}" \ -t "${IMAGE_FRONTEND}:latest" -t "${IMAGE_FRONTEND}:${IMAGE_TAG}" \ --label "org.opencontainers.image.version=${IMAGE_TAG}" \ --label "org.opencontainers.image.source=${GIT_SOURCE}" \ . > "$logfile" 2>&1 local exit_code=$? if [[ $exit_code -eq 0 ]]; then log "✅ Frontend done ($(tail -1 "$logfile" | grep -oP 'DONE \K[0-9.]+s' || echo 'complete'))" else warn "❌ Frontend FAILED - see $logfile" tail -20 "$logfile" fi return $exit_code } build_backend() { local logfile="$LOG_DIR/backend.log" docker build -f apps/bff/Dockerfile \ --build-arg "PNPM_VERSION=${PNPM_VERSION}" \ -t "${IMAGE_BACKEND}:latest" -t "${IMAGE_BACKEND}:${IMAGE_TAG}" \ --label "org.opencontainers.image.version=${IMAGE_TAG}" \ --label "org.opencontainers.image.source=${GIT_SOURCE}" \ . > "$logfile" 2>&1 local exit_code=$? if [[ $exit_code -eq 0 ]]; then log "✅ Backend done ($(tail -1 "$logfile" | grep -oP 'DONE \K[0-9.]+s' || echo 'complete'))" else warn "❌ Backend FAILED - see $logfile" tail -20 "$logfile" fi return $exit_code } # Build images START=$(date +%s) if [[ "$PARALLEL" -eq 1 ]]; then log "🚀 Parallel build (logs: $LOG_DIR/)" log "🔨 Building frontend..." log "🔨 Building backend..." build_frontend & FE_PID=$! build_backend & BE_PID=$! # Show progress dots while waiting while kill -0 $FE_PID 2>/dev/null || kill -0 $BE_PID 2>/dev/null; do printf "." sleep 5 done echo "" # Check results wait $FE_PID || fail "Frontend build failed - check $LOG_DIR/frontend.log" wait $BE_PID || fail "Backend build failed - check $LOG_DIR/backend.log" else log "🔧 Sequential build..." log "🔨 Building frontend..." build_frontend || fail "Frontend build failed" log "🔨 Building backend..." build_backend || fail "Backend build failed" fi BUILD_TIME=$(($(date +%s) - START)) log "⏱️ Built in ${BUILD_TIME}s" # Save tarballs if [[ "$SAVE_TARS" -eq 1 ]]; then mkdir -p "$OUTPUT_DIR" SAVE_START=$(date +%s) # Determine filename suffix if [[ "$USE_LATEST_FILENAME" -eq 1 ]]; then FILE_TAG="latest" else FILE_TAG="$IMAGE_TAG" fi if [[ "$COMPRESS" -eq 1 ]]; then # Pick fastest available compressor: pigz (parallel) > gzip if command -v pigz >/dev/null 2>&1; then COMPRESSOR="pigz -p $(nproc)" # Use all CPU cores COMP_NAME="pigz" else COMPRESSOR="gzip -1" # Fast mode if no pigz COMP_NAME="gzip" fi FE_TAR="$OUTPUT_DIR/${IMAGE_FRONTEND}.${FILE_TAG}.tar.gz" BE_TAR="$OUTPUT_DIR/${IMAGE_BACKEND}.${FILE_TAG}.tar.gz" log "💾 Compressing with $COMP_NAME..." (docker save "${IMAGE_FRONTEND}:latest" | $COMPRESSOR > "$FE_TAR") & (docker save "${IMAGE_BACKEND}:latest" | $COMPRESSOR > "$BE_TAR") & wait else FE_TAR="$OUTPUT_DIR/${IMAGE_FRONTEND}.${FILE_TAG}.tar" BE_TAR="$OUTPUT_DIR/${IMAGE_BACKEND}.${FILE_TAG}.tar" log "💾 Saving uncompressed tarballs..." docker save -o "$FE_TAR" "${IMAGE_FRONTEND}:latest" & docker save -o "$BE_TAR" "${IMAGE_BACKEND}:latest" & wait fi SAVE_TIME=$(($(date +%s) - SAVE_START)) sha256sum "$FE_TAR" > "${FE_TAR}.sha256" sha256sum "$BE_TAR" > "${BE_TAR}.sha256" log "✅ Saved in ${SAVE_TIME}s:" printf " %-50s %s\n" "$FE_TAR" "$(du -h "$FE_TAR" | cut -f1)" printf " %-50s %s\n" "$BE_TAR" "$(du -h "$BE_TAR" | cut -f1)" fi # Push to registry if [[ -n "$PUSH_REMOTE" ]]; then log "📤 Pushing to ${PUSH_REMOTE}..." for img in "${IMAGE_FRONTEND}" "${IMAGE_BACKEND}"; do for tag in "latest" "${IMAGE_TAG}"; do docker tag "${img}:${tag}" "${PUSH_REMOTE}/${img}:${tag}" docker push "${PUSH_REMOTE}/${img}:${tag}" & done done wait log "✅ Pushed" fi TOTAL_TIME=$(($(date +%s) - START)) log "🎉 Complete in ${TOTAL_TIME}s" echo "" info "Next: Upload to Plesk, then:" if [[ "$COMPRESS" -eq 1 ]]; then echo " gunzip -c ${IMAGE_FRONTEND}.${FILE_TAG}.tar.gz | docker load" echo " gunzip -c ${IMAGE_BACKEND}.${FILE_TAG}.tar.gz | docker load" else echo " docker load -i ${IMAGE_FRONTEND}.${FILE_TAG}.tar" echo " docker load -i ${IMAGE_BACKEND}.${FILE_TAG}.tar" fi if [[ "$USE_LATEST_FILENAME" -eq 0 ]]; then echo " Update Portainer with tag: ${IMAGE_TAG}" fi