FASHIONISTAR CI/CD
🔄 Celery Queues Deploy: d915798567b1052828794606dda8e62b4500f337 [GitHub Actions]
607bf71 | # ============================================================================= | |
| # 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 | |