- Added glob dependency to package.json and pnpm-lock.yaml for better file handling. - Updated pnpm-workspace.yaml to include additional built dependencies for improved management. - Refactored Dockerfiles for BFF and Portal to enhance security and optimize build processes. - Improved entrypoint scripts to include better logging and readiness checks for services. - Cleaned up TypeScript configuration files for consistency and alignment with project standards.
468 lines
16 KiB
Bash
Executable File
468 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# 🐳 Build Production Docker Images for Plesk Deployment
|
|
# =============================================================================
|
|
# Features:
|
|
# - Parallel builds with BuildKit
|
|
# - Multi-platform support (amd64/arm64)
|
|
# - Compressed tarballs with SHA256 checksums
|
|
# - Buildx builder for cross-platform builds
|
|
# - Intelligent layer caching
|
|
# =============================================================================
|
|
|
|
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}"
|
|
SAVE_TARS=1
|
|
PLATFORM="${PLATFORM:-linux/amd64}"
|
|
PROGRESS="${PROGRESS:-auto}"
|
|
USE_BUILDX="${USE_BUILDX:-0}"
|
|
CLEAN_CACHE="${CLEAN_CACHE:-0}"
|
|
DRY_RUN="${DRY_RUN:-0}"
|
|
|
|
# =============================================================================
|
|
# Colors and Logging
|
|
# =============================================================================
|
|
if [[ -t 1 ]]; then
|
|
G='\033[0;32m' Y='\033[1;33m' R='\033[0;31m' B='\033[0;34m' C='\033[0;36m' M='\033[0;35m' N='\033[0m'
|
|
else
|
|
G='' Y='' R='' B='' C='' M='' N=''
|
|
fi
|
|
|
|
log() { echo -e "${G}[BUILD]${N} $*"; }
|
|
info() { echo -e "${B}[INFO]${N} $*"; }
|
|
warn() { echo -e "${Y}[WARN]${N} $*"; }
|
|
fail() { echo -e "${R}[ERROR]${N} $*"; exit 1; }
|
|
step() { echo -e "${C}[STEP]${N} $*"; }
|
|
debug() { [[ "${DEBUG:-0}" -eq 1 ]] && echo -e "${M}[DEBUG]${N} $*" || true; }
|
|
|
|
# =============================================================================
|
|
# Usage
|
|
# =============================================================================
|
|
usage() {
|
|
cat <<EOF
|
|
Build Docker images and save tarballs for Plesk deployment.
|
|
|
|
Usage: $0 [OPTIONS]
|
|
|
|
Options:
|
|
--tag <tag> Version tag for image (default: YYYYMMDD-gitsha)
|
|
--output <dir> Output directory (default: project root)
|
|
--push <registry> 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)
|
|
--platform <p> Target platform (default: linux/amd64)
|
|
--buildx Use Docker Buildx for builds (better caching)
|
|
--clean-cache Clean Docker build cache before building
|
|
--dry-run Show what would be done without executing
|
|
--ci CI mode: plain progress output, no colors
|
|
--debug Enable debug output
|
|
-h, --help Show this help
|
|
|
|
Platform Options:
|
|
linux/amd64 Standard x86_64 servers (default)
|
|
linux/arm64 ARM64 servers (Apple Silicon, Graviton)
|
|
|
|
Examples:
|
|
$0 # Output: portal-frontend.latest.tar.gz
|
|
$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
|
|
$0 --platform linux/arm64 # Build for ARM64
|
|
$0 --buildx --clean-cache # Fresh buildx build
|
|
$0 --ci # CI-friendly output
|
|
|
|
Environment Variables:
|
|
IMAGE_FRONTEND_NAME Override frontend image name (default: portal-frontend)
|
|
IMAGE_BACKEND_NAME Override backend image name (default: portal-backend)
|
|
PNPM_VERSION Override PNPM version (default: from package.json)
|
|
NEXT_PUBLIC_API_BASE Next.js API base path (default: /api)
|
|
DEBUG=1 Enable debug output
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
# =============================================================================
|
|
# Argument Parsing
|
|
# =============================================================================
|
|
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 ;;
|
|
--platform) PLATFORM="${2:-linux/amd64}"; shift 2 ;;
|
|
--buildx) USE_BUILDX=1; shift ;;
|
|
--clean-cache) CLEAN_CACHE=1; shift ;;
|
|
--dry-run) DRY_RUN=1; shift ;;
|
|
--ci) PROGRESS="plain"; G=''; Y=''; R=''; B=''; C=''; M=''; N=''; shift ;;
|
|
--debug) DEBUG=1; shift ;;
|
|
-h|--help) usage ;;
|
|
*) fail "Unknown option: $1" ;;
|
|
esac
|
|
done
|
|
|
|
# =============================================================================
|
|
# Validation
|
|
# =============================================================================
|
|
command -v docker >/dev/null 2>&1 || fail "Docker is required but not installed"
|
|
|
|
cd "$PROJECT_ROOT"
|
|
|
|
[[ -f apps/portal/Dockerfile ]] || fail "Missing apps/portal/Dockerfile"
|
|
[[ -f apps/bff/Dockerfile ]] || fail "Missing apps/bff/Dockerfile"
|
|
[[ -f package.json ]] || fail "Missing package.json"
|
|
|
|
# Verify Docker daemon is running
|
|
docker info >/dev/null 2>&1 || fail "Docker daemon is not running"
|
|
|
|
# =============================================================================
|
|
# Setup
|
|
# =============================================================================
|
|
|
|
# Auto-generate tag if not provided
|
|
if [[ -z "$IMAGE_TAG" ]]; then
|
|
GIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo 'local')
|
|
IMAGE_TAG="$(date +%Y%m%d)-${GIT_SHA}"
|
|
fi
|
|
|
|
# Enable BuildKit
|
|
export DOCKER_BUILDKIT=1
|
|
|
|
# Extract PNPM version from package.json (packageManager field)
|
|
PNPM_VERSION_FROM_PKG=$(grep -oP '"packageManager":\s*"pnpm@\K[0-9.]+' package.json 2>/dev/null || echo "")
|
|
PNPM_VERSION="${PNPM_VERSION:-${PNPM_VERSION_FROM_PKG:-10.25.0}}"
|
|
|
|
# Build args
|
|
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)"
|
|
GIT_COMMIT="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
|
|
# Log directory
|
|
LOG_DIR="${OUTPUT_DIR}/.build-logs"
|
|
mkdir -p "$LOG_DIR"
|
|
|
|
# =============================================================================
|
|
# Buildx Setup
|
|
# =============================================================================
|
|
BUILDER_NAME="portal-builder"
|
|
|
|
setup_buildx() {
|
|
if [[ "$USE_BUILDX" -eq 1 ]]; then
|
|
step "Setting up Docker Buildx..."
|
|
|
|
# Check if buildx is available
|
|
if ! docker buildx version >/dev/null 2>&1; then
|
|
warn "Docker Buildx not available, falling back to standard build"
|
|
USE_BUILDX=0
|
|
return
|
|
fi
|
|
|
|
# Create or use existing builder
|
|
if ! docker buildx inspect "$BUILDER_NAME" >/dev/null 2>&1; then
|
|
docker buildx create --name "$BUILDER_NAME" --driver docker-container --bootstrap
|
|
info "Created buildx builder: $BUILDER_NAME"
|
|
else
|
|
docker buildx use "$BUILDER_NAME"
|
|
debug "Using existing buildx builder: $BUILDER_NAME"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# Clean Cache
|
|
# =============================================================================
|
|
clean_cache() {
|
|
if [[ "$CLEAN_CACHE" -eq 1 ]]; then
|
|
step "Cleaning Docker build cache..."
|
|
docker builder prune -f --filter type=exec.cachemount 2>/dev/null || true
|
|
docker builder prune -f --filter unused-for=24h 2>/dev/null || true
|
|
log "✅ Build cache cleaned"
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# Build Functions
|
|
# =============================================================================
|
|
# Build functions moved to build_frontend and build_backend for better argument handling
|
|
|
|
build_frontend() {
|
|
local logfile="$LOG_DIR/frontend.log"
|
|
|
|
step "Building frontend image..."
|
|
|
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
info "[DRY-RUN] Would build frontend"
|
|
return 0
|
|
fi
|
|
|
|
local exit_code=0
|
|
|
|
docker build \
|
|
--load \
|
|
-f apps/portal/Dockerfile \
|
|
--platform "${PLATFORM}" \
|
|
--progress "${PROGRESS}" \
|
|
--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}" \
|
|
--label "org.opencontainers.image.revision=${GIT_COMMIT}" \
|
|
--label "org.opencontainers.image.created=${BUILD_DATE}" \
|
|
. > "$logfile" 2>&1 || exit_code=$?
|
|
|
|
if [[ $exit_code -eq 0 ]]; then
|
|
local size
|
|
size=$(docker image inspect "${IMAGE_FRONTEND}:latest" --format='{{.Size}}' 2>/dev/null | numfmt --to=iec 2>/dev/null || echo "?")
|
|
log "✅ Frontend built (${size})"
|
|
return 0
|
|
else
|
|
warn "❌ Frontend FAILED - see $logfile"
|
|
tail -50 "$logfile" || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
build_backend() {
|
|
local logfile="$LOG_DIR/backend.log"
|
|
|
|
step "Building backend image..."
|
|
|
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
info "[DRY-RUN] Would build backend"
|
|
return 0
|
|
fi
|
|
|
|
local exit_code=0
|
|
|
|
docker build \
|
|
--load \
|
|
-f apps/bff/Dockerfile \
|
|
--platform "${PLATFORM}" \
|
|
--progress "${PROGRESS}" \
|
|
--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}" \
|
|
--label "org.opencontainers.image.revision=${GIT_COMMIT}" \
|
|
--label "org.opencontainers.image.created=${BUILD_DATE}" \
|
|
. > "$logfile" 2>&1 || exit_code=$?
|
|
|
|
if [[ $exit_code -eq 0 ]]; then
|
|
local size
|
|
size=$(docker image inspect "${IMAGE_BACKEND}:latest" --format='{{.Size}}' 2>/dev/null | numfmt --to=iec 2>/dev/null || echo "?")
|
|
log "✅ Backend built (${size})"
|
|
return 0
|
|
else
|
|
warn "❌ Backend FAILED - see $logfile"
|
|
tail -50 "$logfile" || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# Save Tarballs
|
|
# =============================================================================
|
|
save_tarballs() {
|
|
if [[ "$SAVE_TARS" -eq 0 ]] || [[ "$DRY_RUN" -eq 1 ]]; then
|
|
return 0
|
|
fi
|
|
|
|
mkdir -p "$OUTPUT_DIR"
|
|
local save_start
|
|
save_start=$(date +%s)
|
|
|
|
# Determine filename suffix
|
|
local file_tag
|
|
if [[ "$USE_LATEST_FILENAME" -eq 1 ]]; then
|
|
file_tag="latest"
|
|
else
|
|
file_tag="$IMAGE_TAG"
|
|
fi
|
|
|
|
local fe_tar be_tar
|
|
|
|
if [[ "$COMPRESS" -eq 1 ]]; then
|
|
# Pick fastest available compressor: pigz (parallel) > gzip
|
|
local compressor comp_name
|
|
if command -v pigz >/dev/null 2>&1; then
|
|
compressor="pigz -p $(nproc)"
|
|
comp_name="pigz"
|
|
else
|
|
compressor="gzip -1"
|
|
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
|
|
|
|
local save_time
|
|
save_time=$(($(date +%s) - save_start))
|
|
|
|
# Generate checksums
|
|
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)"
|
|
}
|
|
|
|
# =============================================================================
|
|
# Push to Registry
|
|
# =============================================================================
|
|
push_images() {
|
|
if [[ -z "$PUSH_REMOTE" ]] || [[ "$DRY_RUN" -eq 1 ]]; then
|
|
return 0
|
|
fi
|
|
|
|
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 to registry"
|
|
}
|
|
|
|
# =============================================================================
|
|
# Main Execution
|
|
# =============================================================================
|
|
main() {
|
|
local start_time
|
|
start_time=$(date +%s)
|
|
|
|
echo ""
|
|
log "🐳 Customer Portal Docker Build"
|
|
log "================================"
|
|
info "🏷️ Tag: ${IMAGE_TAG}"
|
|
info "📦 PNPM: ${PNPM_VERSION} | Platform: ${PLATFORM}"
|
|
info "📁 Build logs: $LOG_DIR/"
|
|
[[ "$DRY_RUN" -eq 1 ]] && warn "🔍 DRY-RUN MODE - no actual builds"
|
|
echo ""
|
|
|
|
# Setup
|
|
setup_buildx
|
|
clean_cache
|
|
|
|
# Build images
|
|
log "🚀 Starting Docker builds..."
|
|
|
|
if [[ "$PARALLEL" -eq 1 ]]; then
|
|
log "Building frontend & backend in parallel..."
|
|
|
|
build_frontend & FE_PID=$!
|
|
build_backend & BE_PID=$!
|
|
|
|
# Track progress
|
|
local elapsed=0
|
|
while kill -0 $FE_PID 2>/dev/null || kill -0 $BE_PID 2>/dev/null; do
|
|
sleep 10
|
|
elapsed=$((elapsed + 10))
|
|
info "⏳ Building... (${elapsed}s elapsed)"
|
|
done
|
|
|
|
# Check results
|
|
local fe_exit=0 be_exit=0
|
|
wait $FE_PID || fe_exit=$?
|
|
wait $BE_PID || be_exit=$?
|
|
|
|
[[ $fe_exit -ne 0 ]] && fail "Frontend build failed (exit $fe_exit) - check $LOG_DIR/frontend.log"
|
|
[[ $be_exit -ne 0 ]] && fail "Backend build failed (exit $be_exit) - check $LOG_DIR/backend.log"
|
|
else
|
|
log "🔧 Sequential build mode..."
|
|
build_frontend || fail "Frontend build failed - check $LOG_DIR/frontend.log"
|
|
build_backend || fail "Backend build failed - check $LOG_DIR/backend.log"
|
|
fi
|
|
|
|
local build_time
|
|
build_time=$(($(date +%s) - start_time))
|
|
log "⏱️ Build completed in ${build_time}s"
|
|
|
|
# Save and push
|
|
save_tarballs
|
|
push_images
|
|
|
|
# Summary
|
|
local total_time
|
|
total_time=$(($(date +%s) - start_time))
|
|
|
|
echo ""
|
|
log "🎉 Complete in ${total_time}s"
|
|
echo ""
|
|
|
|
# Show next steps
|
|
if [[ "$SAVE_TARS" -eq 1 ]] && [[ "$DRY_RUN" -eq 0 ]]; then
|
|
local file_tag
|
|
[[ "$USE_LATEST_FILENAME" -eq 1 ]] && file_tag="latest" || file_tag="$IMAGE_TAG"
|
|
|
|
info "📋 Next steps for Plesk deployment:"
|
|
echo ""
|
|
echo " 1. Upload tarballs to your server:"
|
|
echo " scp ${IMAGE_FRONTEND}.${file_tag}.tar.gz* ${IMAGE_BACKEND}.${file_tag}.tar.gz* user@server:/path/"
|
|
echo ""
|
|
echo " 2. Load images on the server:"
|
|
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
|
|
echo ""
|
|
echo " 3. Verify checksums:"
|
|
echo " sha256sum -c ${IMAGE_FRONTEND}.${file_tag}.tar.gz.sha256"
|
|
echo " sha256sum -c ${IMAGE_BACKEND}.${file_tag}.tar.gz.sha256"
|
|
echo ""
|
|
if [[ "$USE_LATEST_FILENAME" -eq 0 ]]; then
|
|
echo " 4. Update Portainer with tag: ${IMAGE_TAG}"
|
|
echo ""
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Run main
|
|
main
|