cropintel / docs /DEPLOYMENT.md
Jaithra Polavarapu
CropIntel β€” HF Space deploy (all-in-one app)
889dd1b
|
Raw
History Blame Contribute Delete
6.11 kB
# CropIntel Production Deployment (single VPS)
One Docker container runs both the Next.js web app and the Python inference
service (supervisord manages the two processes). Models are fetched once at
container start from a release zip. Right-sized for a single server β€” no
Kubernetes, no Redis, no external model registry.
## Architecture
```
internet ── Caddy (TLS, :443) ── Next.js (:3050, public)
β”‚ POST /api/predict ──► FastAPI inference
β”‚ GET /api/health ──► service (127.0.0.1:8000,
β”‚ never exposed)
└─ models: ml/models/<crop>/<version>/model.tflite
audit log: data/predictions.jsonl
```
- `app/api/predict/route.ts` validates + rate-limits, then forwards the upload
to the inference service (`ml/serve/inference_app.py`), which keeps all crop
models loaded in memory (TFLite, ~9 MB per crop).
- `GET /api/health` aggregates web liveness + per-crop model readiness β€” point
the compose healthcheck and your uptime monitor at it.
## Prerequisites
- VPS with 2 vCPU / 4 GB RAM (TFLite backend; Keras would need ~4Γ— more)
- Docker + compose plugin
- A domain pointed at the VPS (for TLS)
## First deploy
```bash
git clone <repo> /opt/cropintel && cd /opt/cropintel
# .env β€” models bundle + optional secrets
cat > .env <<'EOF'
CROPINTEL_MODELS_URL=https://github.com/rakshithj09/CropIntel/releases/download/v1/cropintel-models-mobile.zip
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=...
CROPINTEL_ADMIN_TOKEN=<random string> # protects POST /admin/reload
EOF
docker compose -f docker-compose.prod.yml up -d --build
curl -fsS http://localhost:3050/api/health # expect {"web":"ok","inference":{"ready":true,...}}
```
The models zip is produced by:
```bash
python -m ml.scripts.package_models --tflite-only -o cropintel-models-mobile.zip
```
and uploaded to a GitHub Release (or any direct-download URL).
### Reverse proxy (TLS)
Caddy on the host is the simplest option:
```
# /etc/caddy/Caddyfile
yourdomain.example {
reverse_proxy 127.0.0.1:3050
}
```
Caddy sets `X-Forwarded-For` automatically. The in-memory rate limiter keys on
the client IP β€” behind any proxy that does NOT set `X-Forwarded-For`, all
clients share one bucket. Verify your proxy sets it.
## Updating
| What changed | Do |
|---|---|
| Code | `git pull && docker compose -f docker-compose.prod.yml up -d --build` |
| Models (new bundle) | update `CROPINTEL_MODELS_URL`, then `rm ml/models/.cropintel-fetch-ok && docker compose -f docker-compose.prod.yml restart` |
| Models (promote a version already on disk) | see below |
**Gotcha:** `ml/models/.cropintel-fetch-ok` is a sentinel that suppresses
re-downloading the models bundle on every container start. A new bundle URL is
silently ignored until you delete this file.
## Model promotion / rollback
Versions live in `ml/models/<crop>/v1_YYYYMMDD_HHMMSS/`. The serving version is
pinned by `ml/models/<crop>/production.json`; without it, the latest complete
version serves (legacy behavior).
```bash
# status of every crop (serving version, test + external accuracy)
python -m ml.scripts.promote_model --status
# promote (gated on metrics.json accuracy + a passing external_eval.json)
python -m ml.scripts.promote_model --crop rice --version v1_20260612_103000
# instant rollback to the previous pointer
python -m ml.scripts.promote_model --crop rice --rollback
# apply without restarting the container
curl -X POST -H "X-Admin-Token: $CROPINTEL_ADMIN_TOKEN" localhost:8000/admin/reload
```
The promotion gate requires an external evaluation (out-of-training-distribution
images), produced with:
```bash
python -m ml.scripts.test_external --crop rice --path ml/field_test/rice --save-json
```
Never promote on in-dataset test accuracy alone β€” the rice and soybean models
both scored 100% in-dataset while failing badly on external images (shortcut
learning). The honest number is external accuracy.
## Monitoring & logs
- **Uptime**: point an external pinger (UptimeRobot / healthchecks.io free tier)
at `https://yourdomain.example/api/health` every minute. An on-box monitor
cannot alert you when the box itself dies.
- **Process restarts**: `restart: unless-stopped` + supervisord auto-restart
handle crashes; the compose healthcheck flags a wedged container.
- **Process logs**: `docker compose -f docker-compose.prod.yml logs -f`
(json-file driver rotates at 20 MB Γ— 5 files).
- **Prediction audit log**: `data/predictions.jsonl` β€” one line per request
(crop, model version, disease, confidence, entropy, verification status,
image quality, latency, image sha256; no image bytes). Use it for drift
analysis: a rising `not_in_catalog`/`unknown` rate for a crop means the field
distribution is moving away from training.
Rotate it with host logrotate β€” `/etc/logrotate.d/cropintel`:
```
/opt/cropintel/data/predictions.jsonl {
size 50M
rotate 10
copytruncate
compress
missingok
}
```
## Backups
```bash
# nightly at 03:00 β€” models + pointers + audit log, keep 7
0 3 * * * /opt/cropintel/scripts/ops/backup.sh /opt/cropintel /var/backups/cropintel
```
Models are also re-fetchable from the release zip, so this is cheap insurance,
not a disaster-recovery plan. Add an `rclone copy` of `/var/backups/cropintel`
to object storage if you want offsite copies.
## Troubleshooting
| Symptom | Check |
|---|---|
| `/api/health` 503 | `curl localhost:8000/readyz` inside the container β€” shows per-crop load errors |
| "Model not ready" for one crop | that crop has no complete version dir; fetch models or train |
| Predictions slow / queueing | the service is single-worker by design (TFLite interpreters are not thread-safe); sustained load beyond ~10 req/s needs a second look |
| New models bundle ignored | delete `ml/models/.cropintel-fetch-ok` and restart |
| Rate limiting all users together | proxy not setting `X-Forwarded-For` |