#!/bin/bash # ───────────────────────────────────────────────────────────────────────────── # LMS — Docker Compose Build & Run Script # Usage: # ./build.sh # build + start (production) # ./build.sh --no-cache # force full rebuild (no Docker layer cache) # ./build.sh --down # stop and remove containers # ./build.sh --restart # stop, rebuild, and start # ./build.sh --logs # tail logs after starting # ───────────────────────────────────────────────────────────────────────────── set -euo pipefail # ── Colour helpers ──────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' ok() { echo -e "${GREEN}✔${RESET} $*"; } info() { echo -e "${CYAN}▶${RESET} $*"; } warn() { echo -e "${YELLOW}⚠${RESET} $*"; } fail() { echo -e "${RED}✘${RESET} $*" >&2; exit 1; } step() { echo -e "\n${BOLD}${CYAN}══ $* ══${RESET}"; } # ── Parse flags ─────────────────────────────────────────────────────────────── NO_CACHE=false DO_DOWN=false DO_RESTART=false DO_LOGS=false for arg in "$@"; do case $arg in --no-cache) NO_CACHE=true ;; --down) DO_DOWN=true ;; --restart) DO_RESTART=true ;; --logs) DO_LOGS=true ;; --help|-h) sed -n '/^# Usage:/,/^# ─/p' "$0" | grep '^#' | sed 's/^# \?//' exit 0 ;; *) fail "Unknown option: $arg" ;; esac done # ── Change to script directory ──────────────────────────────────────────────── cd "$(dirname "$0")" # ── Helper: check required commands ────────────────────────────────────────── require_cmd() { command -v "$1" &>/dev/null || fail "'$1' is not installed or not in PATH." } require_cmd docker require_cmd docker compose 2>/dev/null || { # Older Docker installs use "docker-compose" instead of "docker compose" require_cmd docker-compose # Shim so the rest of the script uses the right command docker() { if [ "$1" = "compose" ]; then shift; docker-compose "$@"; else command docker "$@"; fi } export -f docker } # ───────────────────────────────────────────────────────────────────────────── # --down: stop and remove containers # ───────────────────────────────────────────────────────────────────────────── if $DO_DOWN; then step "Stopping & removing containers" docker compose down --remove-orphans ok "All containers stopped." exit 0 fi # ───────────────────────────────────────────────────────────────────────────── # Validate .env # ───────────────────────────────────────────────────────────────────────────── step "Checking .env" if [ ! -f .env ]; then warn ".env not found — copying from .env.example" if [ ! -f .env.example ]; then fail ".env.example not found either. Cannot continue." fi cp .env.example .env echo "" warn "Please edit .env and set POSTGRES_PASSWORD and JWT_SECRET_KEY, then re-run this script." exit 1 fi source .env ERRORS=0 check_var() { local name=$1 val=${!1:-} placeholder=${2:-""} if [ -z "$val" ] || { [ -n "$placeholder" ] && [ "$val" = "$placeholder" ]; }; then warn "Missing or placeholder value for ${BOLD}${name}${RESET} in .env" ERRORS=$((ERRORS+1)) fi } check_var POSTGRES_PASSWORD "change_me_strong_password" check_var JWT_SECRET_KEY "change_me_generate_with_secrets_token_hex_32" if [ $ERRORS -gt 0 ]; then echo "" echo -e " ${YELLOW}Tip — generate a JWT secret:${RESET}" echo " python3 -c \"import secrets; print(secrets.token_hex(32))\"" echo "" fail "Fix the above values in .env and re-run." fi ok ".env looks good" # ───────────────────────────────────────────────────────────────────────────── # --restart: tear down first # ───────────────────────────────────────────────────────────────────────────── if $DO_RESTART; then step "Stopping existing containers" docker compose down --remove-orphans ok "Stopped." fi # ───────────────────────────────────────────────────────────────────────────── # Build images # ───────────────────────────────────────────────────────────────────────────── step "Building Docker images" BUILD_ARGS="" $NO_CACHE && BUILD_ARGS="--no-cache" && warn "Building without layer cache (--no-cache)" # shellcheck disable=SC2086 docker compose build $BUILD_ARGS ok "Images built successfully" # ───────────────────────────────────────────────────────────────────────────── # Start stack # ───────────────────────────────────────────────────────────────────────────── step "Starting services" docker compose up -d --remove-orphans # ───────────────────────────────────────────────────────────────────────────── # Wait for backend health # ───────────────────────────────────────────────────────────────────────────── step "Waiting for backend to become healthy" MAX_WAIT=60 ELAPSED=0 INTERVAL=3 printf " " while true; do STATUS=$(docker compose ps --format json backend 2>/dev/null \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('Health','') or d.get('State',''))" 2>/dev/null || echo "") case "$STATUS" in healthy) echo ""; ok "Backend is healthy"; break ;; running) printf "."; sleep $INTERVAL; ELAPSED=$((ELAPSED+INTERVAL)) ;; *) printf "."; sleep $INTERVAL; ELAPSED=$((ELAPSED+INTERVAL)) ;; esac if [ $ELAPSED -ge $MAX_WAIT ]; then echo "" warn "Timed out waiting for backend health check — checking logs:" docker compose logs --tail=20 backend break fi done # ───────────────────────────────────────────────────────────────────────────── # Summary # ───────────────────────────────────────────────────────────────────────────── PORT="${FRONTEND_PORT:-3011}" echo "" echo -e "${BOLD}${GREEN}══════════════════════════════════════════${RESET}" echo -e "${BOLD}${GREEN} ✅ LMS is up!${RESET}" echo -e "${BOLD}${GREEN}══════════════════════════════════════════${RESET}" echo -e " ${BOLD}App URL :${RESET} http://localhost:${PORT}" echo -e " ${BOLD}Backend :${RESET} http://localhost:${PORT}/api/docs (via proxy)" echo "" docker compose ps echo "" # ───────────────────────────────────────────────────────────────────────────── # Tail logs if requested # ───────────────────────────────────────────────────────────────────────────── if $DO_LOGS; then step "Tailing logs (Ctrl+C to stop)" docker compose logs -f fi