Skip to content

Deploy NBA GESTALT Beta

NBA GESTALT beta is served from gestalt.toplines.app on the existing EC2 instance. Nginx serves the static Vite frontend and proxies /api/ to a loopback-only FastAPI service on port 8010.

Storage

s3://nba-gestalt-data is the canonical artifact store.

  • nba-gestalt/releases/<release-id>/ contains the compact API runtime bundle.
  • nba-gestalt/archive/ is reserved for raw and historical parquet data.

The API never reads S3 on a request path. A release is synced to local EC2 disk before the service is restarted.

The release bundle also includes the current roster file, the prior-season final roster snapshot, and the player catalog required by /api/roster-moves. This keeps the interactive roster-moves map independent of the EC2 source checkout's data directory.

Publish a runtime release

From the repository root:

./scripts/publish_gestalt_release.sh

The default release is the promoted NAIL-RAPM v1.2.1.3 artifact. Every numerical cache is namespaced by the immutable model run ID. Materialize all of them before a release so rankings, biographies, historical Lab queries, observed lineups, profile decompositions, and preseason forecasts all use the same fitted model:

uv run nba-build-gestalt-exposure-cohort
uv run nba-build-gestalt-lineup-rankings --all-seasons
uv run nba-build-gestalt-player-team-splits
uv run nba-build-gestalt-response-cache --all-seasons
uv run nba-build-gestalt-historical-profiles
uv run nba-build-gestalt-realized-profiles
uv run nba-build-gestalt-preseason-rankings
uv run nba-validate-gestalt-release

The validator checks model identity, context regularization, the exact profile-padding coefficients, all historical season coverage, lineup-score arithmetic, and consistency between the trained target profiles and their web cache. It then writes a SHA-256 and row-count inventory to artifacts/web/releases/<model-artifact>/<run-id>/bundle_manifest.json. The publish script runs this validation again and refuses to upload an incomplete or mixed bundle. Override the run with GESTALT_RUN_ID only after that run's manifest succeeds.

Deploy on EC2

The server keeps the source checkout at ~/nba-lineup-model and releases under /opt/nba-gestalt/releases/. current is a symlink to the active release.

ssh gestalt-web-server
cd ~/nba-lineup-model
git pull --ff-only
sudo cp deploy/gestalt-web.service /etc/systemd/system/gestalt-web.service
sudo cp deploy/gestalt.nginx.conf /etc/nginx/sites-available/gestalt
sudo cp deploy/gestalt-rate-limits.conf /etc/nginx/conf.d/gestalt-rate-limits.conf
sudo ln -sfn /etc/nginx/sites-available/gestalt /etc/nginx/sites-enabled/gestalt
sudo systemctl daemon-reload
sudo nginx -t
sudo systemctl reload nginx

For a published release:

GESTALT_RELEASE_ID=<release-id> ./scripts/deploy_gestalt_beta.sh

After the Route 53 record for gestalt.toplines.app resolves to the EC2 instance, obtain the TLS certificate:

sudo certbot --nginx -d gestalt.toplines.app

API throttling

Nginx applies per-IP limits before proxying requests to FastAPI:

  • General /api/ routes: 120 requests per minute, with a burst of 30.
  • POST /api/matchups: 30 requests per minute, with a burst of 10.
  • Concurrent API connections: 20 per IP generally and 8 on matchup evaluation.

Excess requests receive 429 Too Many Requests. These limits are deployed by scripts/deploy_gestalt_beta.sh and live in deploy/gestalt-rate-limits.conf. The release script intentionally does not overwrite the Nginx virtual-host file, because Certbot manages its TLS directives after the initial setup.

Verify

curl -fsS http://127.0.0.1:8010/api/health
systemctl status gestalt-web
journalctl -u gestalt-web -f