FASHIONISTAR CI/CD
🔄 Celery Queues Deploy: d915798567b1052828794606dda8e62b4500f337 [GitHub Actions]
607bf71
Raw
History Blame Contribute Delete
15.8 kB
#!/usr/bin/env bash
# =============================================================================
# FASHIONISTAR — Universal Dynamic Entrypoint
# =============================================================================
# Automatically detects the hosting platform and configures the server:
#
# Platform Detection:
# ┌─────────────────────────────────────────────────────────────────────┐
# │ SPACE_ID env set → Hugging Face Spaces → PORT=7860 │
# │ NORTHFLANK_* env set → Northflank → Celery mode │
# │ IS_RENDER_ENV env set → Render.com → PORT env var │
# │ RAILWAY_* env set → Railway.app → PORT env var │
# │ FLY_APP_NAME env set → Fly.io → PORT env var │
# │ ORACLE_CLOUD_* env set → Oracle Cloud VM → PORT=10000 │
# │ DEFAULT → Auto from $PORT or 8000 │
# └─────────────────────────────────────────────────────────────────────┘
#
# Usage (in Dockerfile CMD / platform runtime):
# entrypoint.sh → API server (auto-detects port)
# entrypoint.sh celery-worker → Celery worker mode
# entrypoint.sh celery-beat → Celery beat scheduler mode
# entrypoint.sh migrate → Run migrations only and exit
# entrypoint.sh shell → Django shell
# =============================================================================
set -o errexit
set -o pipefail
set -o nounset
# ── Color helpers ─────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[✓] $*${NC}"; }
log_warn() { echo -e "${YELLOW}[!] $*${NC}"; }
log_error() { echo -e "${RED}[✗] $*${NC}"; }
log_section() { echo -e "\n${BOLD}${CYAN}══════════════════════════════════════${NC}"; echo -e "${BOLD}${CYAN} $*${NC}"; echo -e "${BOLD}${CYAN}══════════════════════════════════════${NC}"; }
# ── Platform Detection ────────────────────────────────────────────────────────
detect_platform() {
if [ -n "${SPACE_ID:-}" ] || [ -n "${SPACE_AUTHOR_NAME:-}" ]; then
echo "huggingface"
elif env | grep -q '^NORTHFLANK_'; then
echo "northflank"
elif [ -n "${RENDER:-}" ] || [ -n "${IS_RENDER_ENV:-}" ]; then
echo "render"
elif [ "${PORT:-}" = "10000" ] || [ "${ORACLE_CLOUD:-}" = "true" ] || [ -n "${ORACLE_CLOUD_INSTANCE_ID:-}" ]; then
echo "oracle"
elif [ -n "${RAILWAY_ENVIRONMENT:-}" ] || [ -n "${RAILWAY_PROJECT_ID:-}" ]; then
echo "railway"
elif [ -n "${FLY_APP_NAME:-}" ]; then
echo "fly"
else
echo "generic"
fi
}
PLATFORM=$(detect_platform)
# ── Port Configuration (per platform) ────────────────────────────────────────
configure_port() {
case "$PLATFORM" in
huggingface)
# Hugging Face REQUIRES port 7860 — non-negotiable
export PORT=7860
log_info "Platform: Hugging Face Spaces | Port: 7860"
;;
northflank)
# Northflank injects PORT automatically (usually 8080 for web services)
export PORT="${PORT:-8080}"
log_info "Platform: Northflank | Port: ${PORT}"
;;
render)
# Render injects PORT automatically (default 10000)
export PORT="${PORT:-10000}"
log_info "Platform: Render.com | Port: ${PORT}"
;;
railway)
# Railway injects PORT automatically
export PORT="${PORT:-8000}"
log_info "Platform: Railway.app | Port: ${PORT}"
;;
fly)
# Fly.io uses 8080 internally
export PORT="${PORT:-8080}"
log_info "Platform: Fly.io | Port: ${PORT}"
;;
oracle)
# Oracle Cloud — use fixed 10000 (matches nginx upstream)
export PORT="${PORT:-10000}"
log_info "Platform: Oracle Cloud | Port: ${PORT}"
;;
*)
# Generic / local dev
export PORT="${PORT:-8000}"
log_info "Platform: Generic/Local | Port: ${PORT}"
;;
esac
# Always ensure gunicorn.conf.py picks up the right port
export GUNICORN_BIND="0.0.0.0:${PORT}"
}
# ── Worker Count (per platform resources) ─────────────────────────────────────
configure_workers() {
local cpu_count
cpu_count=$(nproc 2>/dev/null || echo "2")
case "$PLATFORM" in
huggingface)
# HF free tier: 2 vCPUs → 3 workers (I/O bound formula: 2*CPU+1=5, but use 3 for safety)
export GUNICORN_WORKERS="${GUNICORN_WORKERS:-3}"
;;
northflank)
# Northflank free: limited CPU → 2 workers
export GUNICORN_WORKERS="${GUNICORN_WORKERS:-2}"
;;
render)
# Render free: 512MB → 1-2 workers
export GUNICORN_WORKERS="${GUNICORN_WORKERS:-2}"
;;
*)
# Auto: (2 × CPU) + 1, capped at 9
local auto_workers=$(( (cpu_count * 2) + 1 ))
if [ "$auto_workers" -gt 9 ]; then auto_workers=9; fi
export GUNICORN_WORKERS="${GUNICORN_WORKERS:-$auto_workers}"
;;
esac
log_info "Gunicorn workers: ${GUNICORN_WORKERS} (CPUs detected: ${cpu_count})"
}
# ── Pre-flight Database Migrations ────────────────────────────────────────────
run_migrations() {
log_section "Running Database Migrations"
# Non-fatal: if DB is unreachable (SSL error, connection refused) we continue
# so Gunicorn starts and can serve health checks. Django's health endpoint
# handles DB failure gracefully via the checks system.
if python manage.py migrate --noinput; then
log_info "Migrations complete"
else
log_warn "Migrations failed (DB may be unreachable) — continuing with Gunicorn anyway"
log_warn "The API will start but DB operations may fail until DB is accessible"
fi
}
# ── Static Files Collection ───────────────────────────────────────────────────
run_collectstatic() {
log_section "Collecting Static Files"
python manage.py collectstatic --noinput --clear 2>/dev/null || \
python manage.py collectstatic --noinput
log_info "Static files collected"
}
# ── Ollama Server Startup + Model Pull ───────────────────────────────────────
start_ollama() {
if [ "${OLLAMA_ENABLED:-True}" = "True" ] && command -v ollama >/dev/null 2>&1; then
log_section "Starting Ollama Server (Background)"
# Set Ollama home to a writable location for the appuser
export OLLAMA_HOME="${OLLAMA_HOME:-/home/appuser/.ollama}"
export OLLAMA_MODELS="${OLLAMA_MODELS:-/home/appuser/.ollama/models}"
ollama serve >/dev/null 2>&1 &
local retry=0
until curl -s http://127.0.0.1:11434/api/tags >/dev/null 2>&1 || [ $retry -eq 30 ]; do
sleep 1
retry=$((retry + 1))
done
if curl -s http://127.0.0.1:11434/api/tags >/dev/null 2>&1; then
log_info "Ollama Server is online and responding."
# Pull models if not already cached — runs in background to avoid blocking startup
local OLLAMA_LLM_MODEL="${OLLAMA_MODEL:-llama3.2}"
local OLLAMA_EMBED="${OLLAMA_EMBED_MODEL:-nomic-embed-text}"
(
# Check if LLM model exists, pull if not
if ! ollama list 2>/dev/null | grep -q "${OLLAMA_LLM_MODEL}"; then
log_info "Pulling Ollama LLM model: ${OLLAMA_LLM_MODEL} (background)..."
ollama pull "${OLLAMA_LLM_MODEL}" >/dev/null 2>&1 && \
log_info "Ollama model '${OLLAMA_LLM_MODEL}' ready." || \
log_warn "Failed to pull Ollama model '${OLLAMA_LLM_MODEL}'."
else
log_info "Ollama model '${OLLAMA_LLM_MODEL}' already cached."
fi
# Check if embed model exists, pull if not
if ! ollama list 2>/dev/null | grep -q "${OLLAMA_EMBED}"; then
log_info "Pulling Ollama embed model: ${OLLAMA_EMBED} (background)..."
ollama pull "${OLLAMA_EMBED}" >/dev/null 2>&1 && \
log_info "Ollama model '${OLLAMA_EMBED}' ready." || \
log_warn "Failed to pull Ollama embed model '${OLLAMA_EMBED}'."
else
log_info "Ollama embed model '${OLLAMA_EMBED}' already cached."
fi
) &
else
log_warn "Ollama Server failed to start in 30 seconds. AI features will degrade gracefully."
fi
else
log_info "Ollama disabled (OLLAMA_ENABLED=${OLLAMA_ENABLED:-True}) or not installed — skipping."
fi
}
# ── Main Entrypoint Logic ─────────────────────────────────────────────────────
log_section "FASHIONISTAR Universal Entrypoint"
log_info "Platform detected: ${PLATFORM}"
log_info "Django settings: ${DJANGO_SETTINGS_MODULE:-backend.config.production}"
configure_port
configure_workers
# Handle explicit mode commands
COMMAND="${1:-api}"
# On Northflank, if command is 'api', default to 'celery-worker'
if [ "$PLATFORM" = "northflank" ] && [ "$COMMAND" = "api" ]; then
log_info "Northflank environment detected. Overriding default command 'api' to 'celery-worker'."
COMMAND="celery-worker"
fi
case "$COMMAND" in
# ── Celery Worker Mode ───────────────────────────────────────────────────
celery-worker|worker)
log_section "Starting Celery Worker (${PLATFORM})"
if [ "$PLATFORM" = "huggingface" ]; then
log_info "Hugging Face platform detected. Starting background HTTP health server on port 7860..."
python deploy/huggingface/hf_health_server.py &
fi
# Start Ollama service if enabled
start_ollama
export CELERY_CONCURRENCY="${CELERY_CONCURRENCY:-4}"
export CELERY_QUEUES="${CELERY_QUEUES:-default,ai_tasks,measurements,analytics,notifications,webhooks}"
exec celery -A backend worker \
--loglevel="${CELERY_LOG_LEVEL:-info}" \
--concurrency="${CELERY_CONCURRENCY}" \
-Q "${CELERY_QUEUES}" \
--max-tasks-per-child="${CELERY_MAX_TASKS_PER_CHILD:-100}" \
--without-gossip \
--without-mingle \
--events
;;
# ── Celery Beat Mode ─────────────────────────────────────────────────────
celery-beat|beat)
log_section "Starting Celery Beat Scheduler (${PLATFORM})"
if [ "$PLATFORM" = "huggingface" ]; then
log_info "Hugging Face platform detected. Starting background HTTP health server on port 7860..."
python deploy/huggingface/hf_health_server.py &
fi
exec celery -A backend beat \
--loglevel="${CELERY_LOG_LEVEL:-info}" \
--scheduler django_celery_beat.schedulers:DatabaseScheduler \
--max-interval=10
;;
# ── Celery Worker + Beat Combined (for small free tiers) ─────────────────
celery-all)
log_section "Starting Celery Worker + Beat (${PLATFORM})"
exec celery -A backend worker \
--beat \
--loglevel="${CELERY_LOG_LEVEL:-info}" \
--concurrency=2 \
-Q "default,ai_tasks,measurements,analytics,notifications,webhooks" \
--scheduler django_celery_beat.schedulers:DatabaseScheduler \
--max-tasks-per-child=100 \
--without-gossip \
--without-mingle \
--events
;;
# ── Database Migration Only ───────────────────────────────────────────────
migrate)
run_migrations
log_info "Migration-only run complete. Exiting."
exit 0
;;
# ── Django Management Command passthrough ─────────────────────────────────
manage)
shift
exec python manage.py "$@"
;;
# ── Django Shell ──────────────────────────────────────────────────────────
shell)
exec python manage.py shell
;;
# ── Dev Server (local development only) ───────────────────────────────────
dev|start_dev)
log_warn "Development mode — NOT for production use!"
export PORT="${PORT:-8000}"
python manage.py migrate --noinput
python manage.py collectstatic --noinput
exec python manage.py runserver "0.0.0.0:${PORT}"
;;
# ── API Server (default — production Gunicorn + UvicornWorker) ───────────
api|*)
log_section "Starting FASHIONISTAR API Server (${PLATFORM})"
log_info "Bind address: 0.0.0.0:${PORT}"
log_info "Workers: ${GUNICORN_WORKERS}"
# Start Ollama service if enabled
start_ollama
# Run migrations before starting (idempotent — safe to run each deploy)
run_migrations
# Collect static files (fast — only copies changed files)
run_collectstatic
log_section "🚀 FASHIONISTAR API is Starting!"
log_info "URL: http://0.0.0.0:${PORT}/api/v1/health/"
exec gunicorn \
--workers "${GUNICORN_WORKERS}" \
--worker-class uvicorn.workers.UvicornWorker \
--bind "0.0.0.0:${PORT}" \
--timeout "${GUNICORN_TIMEOUT:-900}" \
--keep-alive "${GUNICORN_KEEPALIVE:-900}" \
--max-requests "${GUNICORN_MAX_REQUESTS:-1000}" \
--max-requests-jitter "${GUNICORN_MAX_REQUESTS_JITTER:-100}" \
--preload \
--access-logfile - \
--error-logfile - \
--log-level "${GUNICORN_LOG_LEVEL:-info}" \
backend.asgi:application
;;
esac