mirror of
https://git.victorphan.net/basketballcantho/ten-project.git
synced 2026-08-05 10:43:11 +07:00
184 lines
9.5 KiB
Bash
Executable File
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:-3000}"
|
|
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
|