polyglot-alpha / deploy /entrypoint.sh
licaomeng
deploy: main@8970ffb β†’ HF Spaces (2026-05-27T05:19Z)
88d2f2a
#!/usr/bin/env bash
# =============================================================================
# PolyglotAlpha β€” container entrypoint (W23, Hugging Face Spaces)
# =============================================================================
# Boots three processes inside one HF Spaces container:
# 1. uvicorn (FastAPI, 127.0.0.1:8000) β€” the backend
# 2. next start (Next.js prod, 127.0.0.1:3001) β€” the UI
# 3. nginx (0.0.0.0:7860) β€” the only port HF exposes externally
#
# A few practical concerns the script handles:
# * If the Space has persistent storage mounted at `/data`, copy the
# seeded SQLite DB there on first boot and re-point DATABASE_URL.
# * Wait for backend health before firing the pre-seed events so the
# leaderboard / events list aren't empty on the reviewer's first visit.
# * Trap SIGTERM/SIGINT so HF's graceful shutdown actually kills children
# (the default `wait` keeps the parent alive but leaves zombies if we
# don't propagate the signal).
# -----------------------------------------------------------------------------
set -euo pipefail
log() {
printf '[%s entrypoint] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >&2
}
# -----------------------------------------------------------------------------
# Persistent storage handling β€” HF Spaces "small persistent storage" tier
# mounts a writable /data volume that survives sleeps + redeploys. Without
# it, the SQLite DB lives in the container FS and resets on every redeploy
# (acceptable for a pure demo; cold starts still pre-seed below).
# -----------------------------------------------------------------------------
if [[ -d /data && -w /data ]]; then
log "persistent /data detected β€” relocating SQLite DB"
if [[ ! -f /data/polyglot_alpha.db ]]; then
if [[ -f /app/polyglot_alpha.db ]]; then
cp /app/polyglot_alpha.db /data/polyglot_alpha.db
log "seeded /data/polyglot_alpha.db from container image"
fi
fi
export DATABASE_URL="sqlite:////data/polyglot_alpha.db"
log "DATABASE_URL=${DATABASE_URL}"
else
log "no /data volume β€” using in-image SQLite (will reset on redeploy)"
fi
# -----------------------------------------------------------------------------
# 1. Backend
# -----------------------------------------------------------------------------
cd /app
log "starting uvicorn on 127.0.0.1:8000"
python -m uvicorn polyglot_alpha.api.main:app \
--host 127.0.0.1 \
--port 8000 \
--log-level info \
--no-access-log &
BACKEND_PID=$!
# -----------------------------------------------------------------------------
# 2. Frontend
# -----------------------------------------------------------------------------
log "starting Next.js on 127.0.0.1:3001"
(
cd /app/ui
exec npx --no-install next start -H 127.0.0.1 -p 3001
) &
FRONTEND_PID=$!
# -----------------------------------------------------------------------------
# Wait for backend health before firing the pre-seed lifecycle. We bound the
# wait at ~60 s so a wedged backend still surfaces (nginx will return 502 on
# /health and HF Spaces will show the failure clearly).
# -----------------------------------------------------------------------------
wait_for_backend() {
local attempts=0
while (( attempts < 60 )); do
if curl -fsS --max-time 2 http://127.0.0.1:8000/health > /dev/null 2>&1; then
log "backend healthy after ${attempts}s"
return 0
fi
sleep 1
attempts=$((attempts + 1))
done
log "backend never reached /health β€” proceeding anyway"
return 1
}
# -----------------------------------------------------------------------------
# Pre-seed events β€” fire 3 mock lifecycles so the leaderboard / events /
# history pages have something to show the first reviewer. We background
# this so nginx can come up immediately. The `|| true` ensures any single
# failure doesn't take down the whole entrypoint.
# -----------------------------------------------------------------------------
preseed_events() {
wait_for_backend || return 0
log "pre-seeding 3 mock events"
local i
for i in 1 2 3; do
curl -fsS -X POST http://127.0.0.1:8000/trigger/event \
-H "Content-Type: application/json" \
-d '{"mode":"mock"}' > /dev/null 2>&1 || \
log "pre-seed event ${i} failed (continuing)"
sleep 4
done
log "pre-seed complete"
}
# Run pre-seed in a `disown`-ed subshell so its natural exit (after 3
# events β‰ˆ 14s) does NOT trigger the `wait -n` teardown below. Only the
# 3 critical processes (backend, frontend, nginx) should bring down
# the container when they die.
( preseed_events ) &
disown $!
# -----------------------------------------------------------------------------
# 3. nginx β€” run in foreground as PID 1's last child so its exit takes down
# the container if proxy config is broken (fail-fast).
# -----------------------------------------------------------------------------
log "starting nginx on 0.0.0.0:7860"
nginx -g 'daemon off;' &
NGINX_PID=$!
# -----------------------------------------------------------------------------
# Signal handling β€” propagate SIGTERM/SIGINT to all children. HF Spaces
# sends SIGTERM with a 30s grace before SIGKILL.
# -----------------------------------------------------------------------------
shutdown() {
log "received shutdown signal β€” forwarding to children"
kill -TERM "$BACKEND_PID" "$FRONTEND_PID" "$NGINX_PID" 2>/dev/null || true
wait "$BACKEND_PID" "$FRONTEND_PID" "$NGINX_PID" 2>/dev/null || true
log "all children exited β€” bye"
exit 0
}
trap shutdown SIGTERM SIGINT
# Wait on the 3 critical PIDs explicitly β€” `wait -n` without args
# would catch ANY background job (including the pre-seed loop), so we
# enumerate just the long-running services. If backend/frontend/nginx
# dies, abort the container so failure surfaces in HF logs.
wait -n "$BACKEND_PID" "$FRONTEND_PID" "$NGINX_PID"
log "a critical child process exited β€” tearing down the rest"
shutdown