lawn-estimator-dev / Dockerfile
TempuraML's picture
Merge Sentry error tracking (#116)
8e47b93
Raw
History Blame Contribute Delete
5.12 kB
# Plain pip wheels throughout β€” the former PDAL dependency (conda-forge only,
# broke Spaces builds) was replaced by laspy + pyproj, verified bit-identical.
# Base image pinned by digest (python:3.11-slim) so a rebuild is reproducible.
FROM python:3.11-slim@sha256:e031123e3d85762b141ad1cbc56452ba69c6e722ebf2f042cc0dc86c47c0d8b3
WORKDIR /app
# CPU-only torch: ~1.5 GB smaller than the default CUDA build. Pinned to match CI;
# torchvision is required by transformers' Mask2Former image processor.
RUN pip install --no-cache-dir torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cpu
# Postgres driver for the durable prod store (P1d). Installed separately from the
# hash-pinned lock (same pattern as torch) β€” a prebuilt binary wheel, no libpq build.
# Used only when DATABASE_URL is a postgres:// URL (Neon); SQLite is the default otherwise.
RUN pip install --no-cache-dir "psycopg[binary]>=3.2,<3.3"
# Clerk session-JWT verification (P2a dashboard auth): PyJWT + cryptography for RS256/JWKS.
# Installed separately from the hash-pinned lock (prebuilt wheels). Used only when
# CLERK_SECRET_KEY is set; otherwise the dashboards run on the dev-token shim.
RUN pip install --no-cache-dir "pyjwt[crypto]>=2.8,<3"
# Cloud Tasks client for serverless batch dispatch (B3). Installed separately from the
# hash-pinned lock. Used only when the CLOUD_TASKS_* env is set (Cloud Run); otherwise batch
# runs on the in-process worker and this stays dormant.
RUN pip install --no-cache-dir "google-cloud-tasks>=2.16,<3"
# S3-compatible client for the optional Cloudflare R2 LiDAR tile cache (A3). Installed
# separately from the hash-pinned lock (prebuilt wheels). Used only when the R2_* env is
# set; otherwise the tile cache is a no-op and tiles come straight from Douglas S3 / USGS.
RUN pip install --no-cache-dir "boto3>=1.34,<2"
# Error tracking (observability MVP). Installed separately from the hash-pinned lock. Used
# only when SENTRY_DSN is set; otherwise Sentry init is a no-op and this stays dormant.
RUN pip install --no-cache-dir "sentry-sdk>=2.0,<3"
# Runtime deps from the hash-pinned lock (uv pip compile of requirements-api.txt).
# --require-hashes refuses anything whose hash isn't listed, so the whole tree is
# reproducible and tamper-evident. Regenerate the lock when requirements-api.txt changes.
COPY requirements-api.lock .
RUN pip install --no-cache-dir --require-hashes -r requirements-api.lock
COPY pyproject.toml ./
COPY src/ src/
RUN pip install --no-cache-dir --no-deps .
# Hugging Face Spaces runs the container as UID 1000 with ephemeral disk.
RUN useradd -m -u 1000 user && mkdir -p /app/data && chown -R user:user /app
USER user
ENV HOME=/home/user
ENV HF_HOME=/home/user/.cache/huggingface
ENV MPLBACKEND=Agg
ENV MPLCONFIGDIR=/tmp/mpl
# Pre-bake the segmentation models into the image so ephemeral hosts don't
# re-download them from Hugging Face after every restart. mask2former (~200 MB)
# is always used; SAM (~375 MB) backs the opt-in SAM_RESTRICT path β€” baking it
# avoids a ~1-2 min download on the first quote after each startup when enabled.
# IDs + revisions are imported from the installed package (single source of truth),
# so the baked weights always match what the code loads at runtime.
RUN python -c "from transformers import AutoImageProcessor, AutoModelForUniversalSegmentation as M; \
from lawn_estimator.segmentation import LAWN_MODEL_ID as i, MODEL_REVISIONS as r; \
AutoImageProcessor.from_pretrained(i, revision=r[i]); \
M.from_pretrained(i, revision=r[i])"
RUN python -c "from transformers import SamModel, SamProcessor; \
from lawn_estimator.restrict import SAM_MODEL as s, SAM_REVISION as v; \
SamModel.from_pretrained(s, revision=v); \
SamProcessor.from_pretrained(s, revision=v)"
# EoMT-DINOv3 (~1.2 GB) backs the opt-in LAWN_CASCADE path β€” without the bake,
# the first cascade quote after every restart pulls it from HF (minutes of stall).
RUN python -c "from transformers import AutoImageProcessor, AutoModelForUniversalSegmentation as M; \
from lawn_estimator.segmentation import CASCADE_PRIMARY_MODEL_ID as i, MODEL_REVISIONS as r; \
AutoImageProcessor.from_pretrained(i, revision=r[i]); \
M.from_pretrained(i, revision=r[i])"
# Statewide parcels GeoPackage (~1.1 GB, private HF dataset) backs the opt-in
# PARCELS_FALLBACK path. HF_TOKEN arrives as a BuildKit secret (Space secret of
# the same name); when absent (e.g. a local build) the bake is skipped and the
# fallback simply reports the file as missing if ever enabled.
RUN --mount=type=secret,id=HF_TOKEN,mode=0444,required=false \
python -c "import os; \
tok = open('/run/secrets/HF_TOKEN').read().strip() if os.path.exists('/run/secrets/HF_TOKEN') else None; \
from huggingface_hub import hf_hub_download; \
hf_hub_download('TempuraML/ne-parcels', 'ne_parcels.gpkg', repo_type='dataset', token=tok, local_dir='data/ne_parcels') if tok else print('HF_TOKEN absent - skipping parcels bake')"
RUN mkdir -p data/lidar/tile_index data/outputs
EXPOSE 8000
CMD ["uvicorn", "lawn_estimator.api:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]