simready-central / Dockerfile
arozantsev's picture
Deploy simready-assets run 20261020-a3cb7f2-captions (GPU variant)
3a31d01 verified
Raw History Blame Contribute Delete
28.4 kB
# deploy: 20261020-a3cb7f2-captions (simready-assets)
# GPU variant of the single-container MVP image for Hugging Face Spaces
# (Docker SDK): everything docker/Dockerfile.hf-space has (OpenSearch + Neo4j
# + Redis + deepsearch-api + asset-graph-service + nginx under supervisord),
# plus a *local* siglip2-triton process instead of routing SigLIP2 embeddings
# to NVCF β€” see docs/development.md#hf-space-design-notes
# for the decisions behind this file. Meant to be deployed to an HF Space running
# on a GPU hardware tier (T4 medium β€” see pipeline/hf_space.py's
# request_space_hardware call for the gpu variant); docker/Dockerfile.hf-space
# remains the free/cpu-basic, NVCF-backed variant β€” this is an additional
# deploy target, not a replacement (`SIGLIP2_BACKEND=nvcf` is unaffected).
#
# Builds the Triton half of this image the same way
# ../usdsearch/docker/Dockerfile.siglip2-triton does β€” FROM the bare, public
# nvcr.io/nvidia/tritonserver image, model_repo/ COPYed in from
# docker/hf-space/siglip2-model/ (vendored into *this* repo by
# scripts/extract_siglip2_model.sh, since HF's remote builder can reach
# neither ../usdsearch nor any private registry) β€” rather than FROM the
# prepackaged nvcr.io/nvidia/usdsearch/siglip2-triton image an earlier
# revision of this file used. Verified live why that prepackaged image was
# dropped: its published 1.4.0 tag bakes **FP32** weights (vision 4.4 GB +
# text 2.7 GB, inspected inside a pulled copy) β€” at this model's baked
# `instance_group: 2`, that's ~14 GB of resident GPU weight memory alone,
# and ~18 GB observed live in `nvidia-smi`, blowing past a T4's 16 GB VRAM.
# ../usdsearch's *current* services/siglip2-triton/model_repo checkout is
# already FP16-exported (vision 2.2 GB + text 1.4 GB β€” half the size, ~7 GB
# at instance_group: 2, comfortably under 16 GB) but was never republished
# under that NGC tag β€” vendoring this checkout's own model_repo directly
# sidesteps waiting on a new publish. See
# scripts/extract_siglip2_model.sh's own header for the full mechanism.
#
# nvcr.io/nvidia/tritonserver itself (the base, not the prepackaged image)
# is on the same *public* NGC catalog path already verified live this
# project's other public pulls (opensearchproject/opensearch, neo4j) use β€”
# no credentials needed, so Hugging Face's own remote builder can pull it.
#
# VERIFIED LIVE on a real GPU (RTX 4090, local β€” not a T4, but same
# execution path): full build, `docker run --gpus all`, all 6 Triton models
# READY, OpenSearch/Neo4j restore from S3, deepsearch-api + asset-graph-service
# healthy through nginx. GPU memory settled at ~10-11 GB (tritonserver
# ~10.3 GB + a smaller deepsearch-api CUDA-context allocation) β€” comfortably
# under a T4's 16 GB, confirming the FP32->FP16 switch actually fixed the
# budget problem, not just on paper. A real `/v3/deepsearch/search` call
# came back with genuine `search_type: text_to_vector` /
# `field: siglip2-embedding` hits (query "a red chair" correctly ranked
# office-chair assets top) β€” the local siglip2-triton round trip is real,
# not just "the process started". LLM query parsing
# (`/v3/deepsearch/llm_parse/query`) and VLM validation
# (`/v3/deepsearch/vlm_validate/search_result`) were also exercised live
# against the already-deployed NVCF Qwen endpoint
# (config/pipeline.env.srp-staging-controlled's LLM_ENDPOINT_BASE_URL) with
# $NVCF_GENAI_RUN_KEY as USDSEARCH_LLM_API_KEY β€” both returned real model
# output (a structured filter parse; a logprob-based
# `P(Yes)=... from first-token log-probs.` verdict), confirming this
# variant's NVCF-backed LLM/VLM path works the same as the NVCF/cpu-basic
# variant's.
# 3.9.0 rather than the pipeline's 3.8.0: 3.8.0's netty-handler 4.2.16
# (GHSA-c4c3-7fpv-j4q5, Critical) and httpcore5 5.4 jars were flagged by the
# nSpect container scan (2026-10-02). The Space bulk-loads the pipeline's
# NDJSON (pipeline/restore.py) rather than restoring a snapshot, so the
# versions don't need to match. Not named OPENSEARCH_VERSION: the jar patches
# below are tied to this exact version, and the pipeline's own
# OPENSEARCH_VERSION used to be passed through as a build-arg (and is still
# set as a Variable on the existing Spaces, which HF would pass along).
ARG HF_SPACE_OPENSEARCH_VERSION=3.9.0
# 5.26 (LTS) rather than the pipeline's 5.23: 5.23's bundled netty/jackson/
# jetty/shiro jars, APOC's json-smart and its JDK 17.0.12 were all flagged
# High by the nSpect container scan (2026-10-02). Same major version, so the
# pipeline's 5.23 dumps still load (verified locally).
ARG NEO4J_VERSION=5.26.31-community
ARG TRITONSERVER_BASE_IMAGE=nvcr.io/nvidia/tritonserver:26.05-py3
ARG PYTHON_IMAGE=python:3.12-bookworm
FROM opensearchproject/opensearch:${HF_SPACE_OPENSEARCH_VERSION} AS opensearch-src
FROM neo4j:${NEO4J_VERSION} AS neo4j-src
FROM ${TRITONSERVER_BASE_IMAGE}
WORKDIR /workspace
RUN mkdir -p /models
# Vendored model repo (see header comment) β€” ensembles, the Python-backend
# tokenizer/preprocessing models, and the two ONNX encoders' *config.pbtxt*
# only. The encoders' actual weight files (~3.6 GB combined β€” the ONNX
# externa-data blobs under each encoder's own 1/ subdirectory) are
# deliberately NOT baked here: a real `deploy-space-gpu` attempt 403'd with
# "Repository storage limit reached (Max: 1 GB)" β€” HF Space git repos have a
# default 1 GB storage quota, well under what baking these in needs. Instead
# they're restored from the artifact S3 bucket at container startup
# (pipeline/hf_space_seed.py --component=siglip2-model, run by
# supervisord-gpu.conf's [program:siglip2-triton] before tritonserver
# itself execs) β€” the same "runtime restore, not baked" pattern this image
# already uses for the OpenSearch index and Neo4j graph, just for a
# corpus-independent static artifact instead of per-run data. Publish
# weights to that S3 path with scripts/publish_siglip2_model_to_s3.sh
# whenever scripts/extract_siglip2_model.sh refreshes the vendored copy.
COPY docker/hf-space/siglip2-model/model_repo/ensemble_image_model /models/ensemble_image_model
COPY docker/hf-space/siglip2-model/model_repo/ensemble_text_model /models/ensemble_text_model
COPY docker/hf-space/siglip2-model/model_repo/image_preprocessing /models/image_preprocessing
COPY docker/hf-space/siglip2-model/model_repo/siglip2_vision_encoder_onnx/config.pbtxt /models/siglip2_vision_encoder_onnx/config.pbtxt
COPY docker/hf-space/siglip2-model/model_repo/siglip2_text_encoder_onnx/config.pbtxt /models/siglip2_text_encoder_onnx/config.pbtxt
RUN mkdir -p /models/siglip2_vision_encoder_onnx/1 /models/siglip2_text_encoder_onnx/1
COPY docker/hf-space/siglip2-model/model_repo/tokenizer /models/tokenizer
# Owned by uid 1000: [program:siglip2-triton] restores the encoder weights into /models as that user. Some Space
# hardware (rtx-pro-6000, 2026-10-21) runs the container as uid 1000 whatever USER says, so nothing may need root.
RUN chown -R 1000:1000 /models
# Python deps for the tokenizer + image_preprocessing Python-backend models β€”
# resolved from pyproject.toml + uv.lock (the source of truth) at build time,
# same as ../usdsearch/docker/Dockerfile.siglip2-triton itself does, so this
# doesn't need a separately-maintained requirements.txt that can drift.
# uv and virtualenv (below) are pinned so the third-party notice is
# reproducible: scripts/generate_third_party_notice.sh reads both pins from
# this file β€” re-run it after bumping either.
COPY docker/hf-space/siglip2-model/pyproject.toml docker/hf-space/siglip2-model/uv.lock /workspace/
RUN pip install --no-cache-dir uv==0.12.22 \
&& uv export --frozen --no-hashes --directory /workspace -o /workspace/requirements.txt \
&& pip install --no-cache-dir -r /workspace/requirements.txt \
&& rm /workspace/requirements.txt
COPY docker/hf-space/siglip2-model/PACKAGE-LICENSES /PACKAGE-LICENSES
# Attribution for anyone who pulls the image: this project's LICENSE plus the
# third-party notices (scripts/generate_third_party_notice.sh). Root-level in
# both build contexts (this repo for bake, the staged Space tree for HF);
# licenses/ stays next to the notice so its relative links resolve.
COPY LICENSE THIRD_PARTY_NOTICE.md /licenses/
COPY licenses /licenses/licenses
# Same two size/vulnerability trims ../usdsearch/docker/Dockerfile.siglip2-triton
# itself applies to this exact base image β€” carried over verbatim rather than
# dropped, since they're cheap and free up real space (~1 GB+ for Nsight
# Systems alone) on top of the FP16 savings above.
RUN rm -rf /usr/local/cuda-13.2/NsightSystems-cli-2026.2.1
USER root
# Non-root runtime uid/gid 1000, same convention docker/Dockerfile.hf-space
# uses for every data-writing process in that image (matches
# opensearchproject/opensearch's own "opensearch" user, 1000:1000). This
# base image is Ubuntu 24.04 ("noble") β€” not verified live whether uid 1000
# is already taken by a user the NGC tritonserver image itself creates, so
# this reuses it (rather than erroring) if it is: --chown=1000:1000 below
# only cares that the numeric id resolves consistently, not which username
# owns it.
RUN if id -u 1000 >/dev/null 2>&1; then \
echo "uid 1000 already exists as $(getent passwd 1000 | cut -d: -f1) β€” reusing it"; \
else \
groupadd -g 1000 nvs && useradd -u 1000 -g 1000 -m -d /home/nvs -s /usr/sbin/nologin nvs; \
fi
# python3.12-venv is requested
# explicitly (Ubuntu splits the venv module out of the base python3.12
# package on some images) since this base is noble rather than bookworm and
# hasn't been checked for what's preinstalled.
# libssl3t64 and the virtualenv upgrade mirror usdsearch 1.5.0-rc.7's
# docker/Dockerfile.siglip2-triton vulnerability fixes for this same base.
RUN apt-get update && apt-get install -y --no-install-recommends \
redis-server supervisor nginx curl python3.12-venv openssl libssl3t64 \
&& rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir --upgrade virtualenv==21.14.5 \
&& rm -f /etc/nginx/sites-enabled/default \
&& sed -i 's|^pid .*|pid /tmp/nginx.pid;|' /etc/nginx/nginx.conf \
&& chown -R 1000:1000 /etc/nginx/conf.d /var/lib/nginx /var/log/nginx
# nginx runs as uid 1000 (supervisord-gpu.conf): the pid file moves to /tmp, and the rendered config, temp and log
# dirs belong to that user. Some Space hardware (rtx-pro-6000, 2026-10-21) runs the container as uid 1000 whatever
# USER says, and nginx with user=root never started there (supervisord: "Can't drop privilege as nonroot user").
# The app venv: same /app/.venv path docker/Dockerfile.hf-space and
# docker/hf-space/supervisord*.conf both hardcode. Installed from the same
# repacked usdsearch wheels + constraints.txt as the NVCF variant β€” see that
# Dockerfile's header comment for how wheels/ is produced and refreshed.
# Upstream's own docker/Dockerfile.usdsearch compiles these wheels in a
# python:3.12-bookworm builder stage but runs them in a
# nvcr.io/nvidia/base/ubuntu:noble-20260217 *runtime* stage β€” the same OS
# family as this Triton image's own base β€” so installing them here follows
# upstream's own production direction (older-glibc build -> newer-glibc
# run), not a new cross-distro risk. Not yet verified live in this repo,
# though (see header comment).
COPY docker/hf-space/wheels/ /tmp/usdsearch-wheels/
# env -u PYTHONPATH: with the Triton base image's PYTHONPATH (DALI's wheel dir) visible, pip treated
# packages found there as installed and the venv came out without six (2026-09-29).
# The venv APIs start without PYTHONPATH (supervisord-gpu.conf), so the venv must be complete.
RUN python3.12 -m venv /app/.venv \
&& env -u PYTHONPATH /app/.venv/bin/pip install --no-cache-dir \
--find-links /tmp/usdsearch-wheels \
--constraint /tmp/usdsearch-wheels/constraints.txt \
/tmp/usdsearch-wheels/*.whl \
&& rm -rf /tmp/usdsearch-wheels \
&& chown -R 1000:1000 /app/.venv
# The vision checks' prepared asset views on disk (docker/hf-space/check_image_cache.py), on while the
# CHECK_IMAGE_CACHE_DIR Space Variable is set (pipeline/config.py). The .pth loads it in every venv python. The
# build fails unless it wraps the vendored llm_client and cached views come back byte-identical (--self-test), as PNG
# (the default) and as JPEG (USDSEARCH_VISION_VALIDATION_IMAGE_FORMAT=jpeg, the Space Variable prod runs with).
COPY docker/hf-space/check_image_cache.py docker/hf-space/check_image_cache.pth /tmp/check-image-hook/
RUN SP=$(env -u PYTHONPATH /app/.venv/bin/python -c 'import sysconfig; print(sysconfig.get_path("purelib"))') \
&& install -m 644 /tmp/check-image-hook/check_image_cache.py /tmp/check-image-hook/check_image_cache.pth "$SP/" \
&& rm -rf /tmp/check-image-hook \
&& env -u PYTHONPATH CHECK_IMAGE_CACHE_DIR=/tmp/check-image-selftest /app/.venv/bin/python -m check_image_cache --self-test \
&& env -u PYTHONPATH CHECK_IMAGE_CACHE_DIR=/tmp/check-image-selftest USDSEARCH_VISION_VALIDATION_IMAGE_FORMAT=jpeg \
/app/.venv/bin/python -m check_image_cache --self-test \
&& rm -rf /tmp/check-image-selftest
# hf-storage-api β€” see docker/Dockerfile.hf-space's own copy of this block
# for the full explanation (identical here; this variant serves the same
# corpus/storage backend, just with a different SigLIP2 backend).
COPY docker/hf-space/hf-storage-api/hf_storage_api /app/hf-storage-api/src/hf_storage_api
RUN GRPCIO_VERSION=$(env -u PYTHONPATH /app/.venv/bin/pip show grpcio | sed -n 's/^Version: //p') \
&& env -u PYTHONPATH /app/.venv/bin/pip install --no-cache-dir "grpcio-health-checking==${GRPCIO_VERSION}" \
&& env PYTHONPATH=/app/hf-storage-api/src /app/.venv/bin/python -c "import hf_storage_api.server" \
&& chown -R 1000:1000 /app/hf-storage-api
COPY --from=opensearch-src --chown=1000:1000 /usr/share/opensearch /usr/share/opensearch
# Keep only opensearch-knn: deepsearch-api's hybrid search fuses results
# client-side and the restored index uses only core analyzers + index.knn.
# The other bundled plugins carry netty/httpcore5 jars flagged High by the
# nSpect container scan (2026-10-02). Several passes because a plugin can't
# be removed while another still extends it. With opensearch-security gone,
# the entrypoint skips its DISABLE_SECURITY_PLUGIN handling.
RUN cd /usr/share/opensearch \
&& for pass in 1 2 3; do \
for p in $(ls plugins); do \
[ "$p" = opensearch-knn ] || bin/opensearch-plugin remove "$p" >/dev/null 2>&1 || true; \
done; \
done \
&& [ "$(ls plugins)" = opensearch-knn ] || { echo "plugins left: $(ls plugins)" >&2; exit 1; }
# Jackson jars no OpenSearch release fixes yet (nSpect, 2026-10-02):
# - lib/jackson-core 3.2.2 -> 3.2.3 (GHSA-7hhh-6rmp-j9qf), a patch-level
# drop-in from Maven Central, sha256-pinned. The old jar is removed by exact
# name, so a base bump that already changed it fails here β€” re-check then.
# - modules/ingest-geoip (jackson-databind/-core 2.22.2: GHSA-cxp5-3px4-pw24,
# GHSA-wv8q-qhhj-9h54, ...) removed: no ingest pipeline uses GeoIP.
RUN cd /usr/share/opensearch \
&& rm -r modules/ingest-geoip lib/jackson-core-3.2.2.jar \
&& curl -fsSL -o lib/jackson-core-3.2.3.jar \
https://repo1.maven.org/maven2/tools/jackson/core/jackson-core/3.2.3/jackson-core-3.2.3.jar \
&& echo "a0c17e7e665a5e95c82f5fb45c0fec2ae3dc556c80f0016452c9c84cd23f3817 lib/jackson-core-3.2.3.jar" | sha256sum -c - \
&& chown 1000:1000 lib/jackson-core-3.2.3.jar
# See docker/Dockerfile.hf-space's own (much longer) comment on this same
# block for the full k-NN/faiss library-path story β€” copied verbatim here
# since it's the identical opensearchproject/opensearch source image and the
# identical cross-image-copy cause.
ENV OPENSEARCH_HOME=/usr/share/opensearch \
OPENSEARCH_PATH_CONF=/usr/share/opensearch/config \
PATH="/usr/share/opensearch/bin:/app/.venv/bin:${PATH}" \
LD_LIBRARY_PATH=/usr/share/opensearch/plugins/opensearch-knn/lib \
discovery.type=single-node \
DISABLE_SECURITY_PLUGIN=true \
DISABLE_INSTALL_DEMO_CONFIG=true \
bootstrap.memory_lock=false \
OPENSEARCH_JAVA_OPTS="-Xms1g -Xmx1g -Djava.library.path=/usr/share/opensearch/plugins/opensearch-knn/lib"
# A Category pick sends one filter value per leaf, two Lucene clauses each, so
# the default cap (1,024) refused 513+ values with HTTP 400 (2026-10-19:
# Industrial + Residential + OEM = 519). 8192 takes ~4,000 values.
ENV indices.query.bool.max_clause_count=8192
COPY --from=neo4j-src /opt/java/openjdk /opt/java/openjdk
COPY --from=neo4j-src /var/lib/neo4j /var/lib/neo4j
# Jackson jars no Neo4j release fixes yet (nSpect, 2026-10-02 β€” 2026.09.0
# ships the same ones):
# - lib/jackson-core/-databind 2.22.2 -> 2.22.3 (GHSA-p6pp-m3f8-5c89,
# GHSA-7hhh-6rmp-j9qf, GHSA-cxp5-3px4-pw24, GHSA-wv8q-qhhj-9h54), patch-level
# drop-ins from Maven Central, sha256-pinned; old jars removed by exact name.
# - lib/parquet-jackson removed: it shades jackson 2.22.1 (same advisories)
# and has no fixed release. Only neo4j-admin's Parquet *import* uses it;
# this image only runs `neo4j-admin database load` and the server.
RUN cd /var/lib/neo4j/lib \
&& rm jackson-core-2.22.2.jar jackson-databind-2.22.2.jar parquet-jackson-1.18.0.jar \
&& curl -fsSL -o jackson-core-2.22.3.jar \
https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.22.3/jackson-core-2.22.3.jar \
&& curl -fsSL -o jackson-databind-2.22.3.jar \
https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.22.3/jackson-databind-2.22.3.jar \
&& printf '%s %s\n' \
8a501126a385b25841915d839508f8a66e2a0dbc8a6709d055ef3b3e852b094c jackson-core-2.22.3.jar \
556db5439e206114346043f68d200497dc96a0bca62a360a81784092ebd0e0a9 jackson-databind-2.22.3.jar \
| sha256sum -c -
RUN mkdir -p /data /logs && chmod 777 /data /logs
RUN cp /var/lib/neo4j/labs/apoc-*-core.jar /var/lib/neo4j/plugins/
RUN { \
echo "dbms.security.auth_enabled=false"; \
echo "server.memory.heap.initial_size=1G"; \
echo "server.memory.heap.max_size=1G"; \
echo "server.memory.pagecache.size=512M"; \
} >> /var/lib/neo4j/conf/neo4j.conf
ENV NEO4J_HOME=/var/lib/neo4j \
PATH="/var/lib/neo4j/bin:/opt/java/openjdk/bin:${PATH}"
COPY docker/hf-space/logging.yml /logging.yml
COPY docker/hf-space/search_fields.yaml /app/search_fields.yaml
# The query parser's system prompt β€” same as docker/Dockerfile.hf-space, see its comment.
COPY docker/hf-space/llm_parse_prompt.txt /app/llm_parse_prompt.txt
# The GPU-variant supervisord config β€” same six programs as
# docker/hf-space/supervisord.conf plus [program:siglip2-triton], and
# deepsearch-api's own command additionally waits on Triton's readiness
# before execing uvicorn. See that file's header for the port-collision
# reasoning (Triton's default HTTP port 8000 clashes with deepsearch-api's
# own uvicorn; remapped to 8003 here).
COPY docker/hf-space/supervisord-gpu.conf /etc/supervisor/conf.d/hf-space.conf
# nginx's optional readiness gate (LIBRARY_MEDIA_GATE; see the script's header).
COPY docker/hf-space/media-gate.sh /usr/local/bin/media-gate.sh
COPY docker/hf-space/nginx.conf.template /etc/nginx/hf-space.conf.template
COPY services/explorer/dist /usr/share/nginx/explorer
COPY --chown=1000:1000 pipeline /app/hf-space-pipeline/pipeline
# package-download (supervisord's program of that name) runs from this copy on the
# venv deepsearch-api already brought (fastapi, uvicorn, grpcio, the storage_api
# stubs, search_utils): fail the build, not the Space, if that ever stops importing.
RUN cd /app/hf-space-pipeline && env -u PYTHONPATH /app/.venv/bin/python -c "import pipeline.package_download.app, pipeline.package_download.source, pipeline.package_download.sizes, search_utils.url_mapping"
ARG ARTIFACT_S3_URI
ARG ARTIFACT_S3_REGION=us-east-2
ARG CORPUS_NAME
ENV ARTIFACT_S3_URI=${ARTIFACT_S3_URI} \
ARTIFACT_S3_REGION=${ARTIFACT_S3_REGION} \
CORPUS_NAME=${CORPUS_NAME}
# ─── deepsearch-api config baked for this corpus/run ────────────────────────
# Same as docker/Dockerfile.hf-space's equivalent block, minus every
# NVCF-specific ARG/env (SIGLIP2_NVCF_ENDPOINT/FUNCTION_ID and their
# downstream SIGLIP2_TRITON_SERVER_* values) β€” this variant never talks to
# NVCF at all. SIGLIP2_TRITON_SERVER_URL instead points at the local
# siglip2-triton process's gRPC port (8001, loopback-only β€” nginx remains
# the sole externally-proxied port, same as every other backend process in
# this image), with no SSL and no auth headers, since nothing external ever
# reaches it.
ARG ES_NAME
ARG S3_STORAGE_BUCKET_NAME
ARG S3_STORAGE_REGION_NAME=us-west-2
ARG STORAGE_BACKEND_TYPE=s3
# DOWNLOAD_ENABLED / HF_STORAGE_API_TOKEN / SUBMIT_CONTENT_URL / STORAGE_API_* / DOWNLOAD_* /
# PACKAGE_DOWNLOAD_* β€” see
# docker/Dockerfile.hf-space's own copies of these comments for the full
# explanation.
# USDSEARCH_URL_MAPPING_MODE/_RULES β€” hf_space_build_args() derives them from
# the repos (and pinned revisions) this run indexed; see url_mapping_rules().
ARG USDSEARCH_URL_MAPPING_MODE=off
ARG USDSEARCH_URL_MAPPING_RULES=[]
ARG DOWNLOAD_ENABLED=false
ENV HF_STORAGE_API_TOKEN= \
DOWNLOAD_ENABLED=${DOWNLOAD_ENABLED} \
SUBMIT_CONTENT_URL=https://huggingface.co/datasets/nvidia/simready-central/discussions \
STORAGE_BACKEND_TYPE=${STORAGE_BACKEND_TYPE} \
S3_STORAGE_BUCKET_NAME=${S3_STORAGE_BUCKET_NAME} \
S3_STORAGE_REGION_NAME=${S3_STORAGE_REGION_NAME} \
opensearch_host=http://localhost:9200 \
opensearch_index_name=${ES_NAME}-ver5.0 \
opensearch_image_cache_index_name=${ES_NAME}-ver4.0-image-cache \
opensearch_use_ssl=false \
REDIS_URL=redis://localhost:6379 \
DEEPSEARCH_BACKEND_ASSET_GRAPH_SERVICE_URL=http://127.0.0.1:8010 \
ASSET_GRAPH_SERVICE_ENDPOINT=http://127.0.0.1:8010 \
ENABLE_ACCESS_VERIFICATION=False \
STORAGE_REQUIRE_AUTH=False \
VERIFY_ASSET_EXISTENCE=False \
verify_access=false \
ASSERT_ADMIN_USER=false \
USDSEARCH_VISION_VALIDATION_ENABLED=false \
USDSEARCH_LLM_PARSING_ENABLED=false \
# Validation limits are per deepsearch-api process: 32 checks in flight and a 31k-verdict cache in
# each of the 16 processes (DEEPSEARCH_API_PROCESSES below), 512 and ~500k in all (Redis keeps every verdict
# anyway). One search's page runs at most 16 at once (usdsearch's BATCH_CONCURRENCY). 2026-10-20 lab, l4x4
# shape with the check-image cache: 16 per process capped the engine side at ~155 checks/s (p95 of all badges
# 14 s at 80 users); 32 held 3.2 s up to 240 users.
USDSEARCH_VISION_VALIDATION_MAX_CONCURRENT=32 \
USDSEARCH_VISION_VALIDATION_CACHE_SIZE=31250 \
USDSEARCH_LLM_PARSING_FIELDS_FILEPATH=/app/search_fields.yaml \
USDSEARCH_LLM_PARSING_PROMPT_FILEPATH=/app/llm_parse_prompt.txt \
# Business telemetry β€” same as docker/Dockerfile.hf-space, see its comment.
OTEL_SDK_DISABLED=false \
USE_SEARCH_TELEMETRY=true \
OTEL_TRACES_EXPORTER=false \
BACKEND_CONFIG_STR="backend_name: es-sample" \
DEFAULT_CUTOFF_THRESHOLD=0 \
HYBRID_SEARCH_SCORING_CONFIG_VECTOR_FIELDS="{\"siglip2-embedding.embedding\":{\"enabled\":true,\"weight\":1,\"field_name\":\"siglip2-embedding.embedding\",\"dimension\":1536}}" \
# Text-leg default β€” same as docker/Dockerfile.hf-space, see its comment.
HYBRID_SEARCH_HYBRID_TEXT_CONFIG_WEIGHT=0.9 \
HYBRID_SEARCH_HYBRID_TEXT_CONFIG_CROSS_FIELD_OPERATOR=or \
HYBRID_SEARCH_HYBRID_TEXT_CONFIG_FIELDS="[{\"field\":\"name\",\"nested\":false,\"enabled\":true,\"weight\":2,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"name.simple\",\"nested\":false,\"enabled\":true,\"weight\":2,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"name.standard\",\"nested\":false,\"enabled\":true,\"weight\":2,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"usd_properties.value\",\"nested\":true,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"usd_properties.key\",\"nested\":true,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"tags.tag\",\"nested\":true,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"tags.value\",\"nested\":true,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"path\",\"nested\":false,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"path.tree\",\"nested\":false,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"path.tree_reversed\",\"nested\":false,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"__VISION_METADATA_FIELDS__\",\"nested\":false,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"name\",\"nested\":false,\"enabled\":true,\"weight\":5,\"match_type\":\"exact\",\"analyzer\":\"keyword\",\"wildcard\":false},{\"field\":\"name.standard\",\"nested\":false,\"enabled\":true,\"weight\":5,\"match_type\":\"exact\",\"analyzer\":\"keyword\",\"wildcard\":false}]" \
USDSEARCH_URL_MAPPING_MODE=${USDSEARCH_URL_MAPPING_MODE} \
USDSEARCH_URL_MAPPING_RULES=${USDSEARCH_URL_MAPPING_RULES} \
STORAGE_API_GRPC_ENDPOINT=127.0.0.1:50051 \
STORAGE_API_SSL=false \
STORAGE_API_IGNORE_FILEFOLDER_API=true \
DOWNLOAD_MAX_CONCURRENT=2 \
DOWNLOAD_QUEUE_TIMEOUT=120 \
DOWNLOAD_MAX_BUNDLE_BYTES=2000000000 \
PACKAGE_DOWNLOAD_PORT=8020 \
PACKAGE_DOWNLOAD_MAX_CONCURRENT=4 \
PACKAGE_DOWNLOAD_QUEUE_TIMEOUT=120 \
PACKAGE_DOWNLOAD_MAX_BUNDLE_BYTES=2000000000 \
STORAGE_API_THUMBNAIL_METADATA_FIELDS="[\"hf:thumbnail_url\"]" \
SIGLIP2_TRITON_SERVER_URL=localhost:8001 \
TRITON_SERVER_URL=localhost:8001 \
SIGLIP2_TRITON_SERVER_SSL=false \
# deepsearch-api processes, ONE uvicorn worker each, behind nginx's least_conn
# upstream (supervisord-gpu.conf, nginx.conf.template). 16 for l4x4 (48 vCPU):
# 2026-10-20 lab, l4x4 shape, 32 checks each: 16 processes held p95 of all badges at
# 3.2 s up to 240 users (6.3 s at 300, CPU-bound); 6 processes reached ~100 users
# with the CPU under 30 %. On a10g-large (12 vCPU) use 6: 8 cost ~20 % more CPU there. Must equal supervisord-gpu.conf's
# numprocs: nginx renders its upstream from this, so never set it as a Space
# Variable. DEEPSEARCH_API_WORKERS is no longer read (a leftover Space Variable
# of that name does nothing).
DEEPSEARCH_API_PROCESSES=16 \
PORT=7860
ARG RUN_ID
ARG CORPUS_SHA256
ARG USDSEARCH_GIT_SHA
LABEL org.opencontainers.image.title="usdsearch-hf-space-simready-central-gpu" \
org.opencontainers.image.description="Single-container MVP (OpenSearch+Neo4j+Redis+deepsearch-api, SigLIP2 via local Triton on GPU) for Hugging Face Spaces" \
com.nvidia.usdsearch.run_id="${RUN_ID}" \
com.nvidia.usdsearch.corpus_sha256="${CORPUS_SHA256}" \
com.nvidia.usdsearch.usdsearch_git_sha="${USDSEARCH_GIT_SHA}"
# Longer start-period than docker/Dockerfile.hf-space's 10m: this variant
# additionally waits on Triton loading ~3.6 GB of ONNX weights onto the GPU
# before deepsearch-api's own uvicorn execs (see supervisord-gpu.conf).
# Verified live against a small (1488-asset) dev corpus: Triton model load
# took ~10s and the whole stack was healthy within ~20s total β€” 15m is
# deliberately still a generous upper bound, not a tight one, since a larger
# production corpus's OpenSearch bulk load / Neo4j offline load (the same
# restore path docker/Dockerfile.hf-space already budgets 10m for) can take
# much longer than this dev corpus did.
HEALTHCHECK --interval=30s --timeout=5s --start-period=15m --retries=3 \
CMD curl -sf "http://localhost:${PORT}/health" || exit 1
EXPOSE 7860
ENTRYPOINT ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/hf-space.conf"]