Files

184 lines
9.5 KiB
Bash
Executable File

#!/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