Files
Nicolò Boschi 5f9bff9056 fix(docker): drop curl from the API runtime images (#4202)
* fix(docker): drop curl from the API runtime images

curl's only in-container consumer was the readiness loop in start-all.sh;
there is no HEALTHCHECK instruction anywhere. It is also the sole
reverse-dependency of libcurl4t64, which brings libssh2-1t64, so one line in
each install list accounted for nine HIGH findings - all status=affected with
no Debian fix published, so `apt-get upgrade` could not clear them and not
shipping the package was the only remediation.

Trivy 0.74.0 HIGH+CRITICAL, on locally built slim images:

  api-only     3C / 60H -> 3C / 51H
  standalone   3C / 60H -> 3C / 51H

with exactly the curl, libcurl4t64 and libssh2-1t64 findings removed and
nothing new.

Replace it with http_probe, which reproduces `curl -sf` WITHOUT -L rather than
approximating it. The distinction matters: curl does not follow redirects
unless asked, so a 302 is a completed transfer and succeeds regardless of what
it points at. urllib.request.urlopen follows it and raises on a 404 behind it,
which would report a healthy service that redirects as "not ready". http_probe
uses http.client and tests `status < 400` itself, bypassing urllib's redirect
handler, and carries userinfo through as Basic auth the way curl does.

Verified equivalent to `curl -sf` on 2xx, 3xx-to-good, 3xx-to-bad, 4xx, 5xx,
query strings, userinfo auth and connection refused. Exit codes are not
reproduced (curl's 22 and 7 become 1); every call site tests zero/non-zero.

One deliberate difference, since it is a change and not a translation: curl was
called with --connect-timeout, which caps only the connection phase, and the
API health loop passed no timeout at all - so a server that accepted a
connection and never answered hung the probe forever. The timeout now covers
the whole request.

There is no wget fallback. BusyBox wget cannot reproduce these semantics (no
--max-redirect, so it always follows), and it is not needed: every image that
probes anything is Python-based. cp-only, the one image with neither, performs
no probe at all and dropped curl in #4197. Missing python3 now fails loudly at
startup instead of degrading into a readiness loop that can never succeed.

Closes #4198

* refactor(docker): move the readiness probe into hindsight_api.http_probe

The first version of this probe was Python embedded in a shell string inside
start-all.sh. That was a bad shape for code encoding rules this fiddly: every
quote had to survive two levels of escaping, ruff and ty never saw it, and it
could only be exercised through the shell.

Move it to hindsight_api/http_probe.py, shipped with the code and covered by
tests/test_http_probe.py, which pins each case to what `curl -sf` does for the
same response. start-all.sh keeps a three-line wrapper that shells out to
`python3 -m hindsight_api.http_probe`.

hindsight-admin was the obvious home and is the wrong one: it takes 5.1s to
start in the built image, against 0.028s for bare stdlib, because it pulls in
the CLI and everything behind it. The readiness loop polls once per second, so
importing the API to ask whether the API is up would break the loop it drives.
This module imports stdlib only; measured 0.035s per probe in the image.
`hindsight_api/__init__` is cheap by design and has to stay that way for this
to hold - its docstring already says so.

The shell test drops to checking the wiring, since the semantics now have a
real home, and skips when the package is not importable: test-start-all.sh also
runs in CI from a bare checkout with no virtualenv.

Reformatting by `ruff format` on first contact is the point - the embedded
version could never have received it.

* refactor(probe): make the readiness probe its own package, isolated from the API

hindsight_api.http_probe was the wrong home. The probe answers "is an API
process up?", and living inside the package it probes invited exactly the
coupling that would break it: an import of the engine or the config would put
API startup cost - and API startup side effects - on a loop that runs once a
second.

Move it to hindsight_probe, a sibling top-level package in the same
distribution. Its dependencies are now explicit by construction: none. It
imports the standard library and nothing else.

Packaging alone does not enforce that. Both packages install into the same
virtualenv, so `import hindsight_api` from the probe would still resolve at
runtime. So the rule is a test, not a convention:
test_imports_nothing_but_the_standard_library imports the package in a clean
subprocess and asserts that nothing outside sys.stdlib_module_names was pulled
in. Adding `import hindsight_api` to the probe fails it with the offending name.

The audit ignores _sysconfigdata_*, a platform-specific stdlib internal whose
name embeds the build triple and so is absent from stdlib_module_names
everywhere.

Both Dockerfiles now copy the package; the api-builder previously copied only
hindsight_api, so the first build without this shipped an image whose probe
could not import. That surfaced as require_http_probe_runtime failing at
startup with a clear message rather than a readiness loop that could never
succeed, which is what that guard is for.

Verified in the built Linux image: no non-stdlib imports, hindsight_api never
loaded, 0.036s per probe, and the full end-to-end boot still reaches
"Hindsight is running" with /health answering 200.

* refactor(probe): keep the readiness probe inside hindsight_api

Reverts the separate hindsight_probe package. It was justified on a bad
measurement: an earlier cold-cache timing suggested `import hindsight_api` cost
~0.12s against ~0.03s for a standalone package. Measured properly, warm, in the
built image, they are the same - ~0.03s each - and importing hindsight_api
pulls in zero third-party modules. Its PEP 562 lazy-attribute design already
does the work the split was meant to do, so the split bought nothing and cost a
second top-level package, four pyproject entries and a COPY in each Dockerfile.

What was worth keeping is the enforcement, which is orthogonal to where the
module lives. test_imports_nothing_heavy imports the probe in a clean
subprocess and asserts it pulled in no third-party package and nothing from
hindsight_api.engine, .api or .config. Adding `from hindsight_api.engine import
memory_engine` to the probe fails it with 43 packages named, numpy, sqlalchemy
and asyncpg among them - which is the failure mode the rule exists to prevent.

Verified in the built image: no third-party or engine imports, 0.034s per
import, probe wiring works, curl absent.
2026-09-08 11:11:04 +02:00

448 lines
20 KiB
Docker

# Hindsight Docker Image
# Supports building API-only, Control Plane-only, or both
#
# Build args:
# INCLUDE_API=true/false - Include API (default: true)
# INCLUDE_CP=true/false - Include Control Plane (default: true)
# INCLUDE_LOCAL_MODELS=true/false - Include local ML models for embeddings/reranking (default: true)
# Set to false when using external providers (TEI, OpenAI, Cohere)
# PRELOAD_ML_MODELS=true/false - Pre-download ML models during build (default: true)
# Only effective when INCLUDE_LOCAL_MODELS=true
#
# Examples:
# docker build -t hindsight . # Both (standalone)
# docker build -t hindsight-api --build-arg INCLUDE_CP=false . # API only
# docker build -t hindsight-cp --build-arg INCLUDE_API=false . # Control Plane only
# docker build -t hindsight --build-arg PRELOAD_ML_MODELS=false . # Skip ML model preload
# docker build -t hindsight --build-arg INCLUDE_LOCAL_MODELS=false . # Skip local ML deps (for external providers)
ARG INCLUDE_API=true
ARG INCLUDE_CP=true
ARG PRELOAD_ML_MODELS=true
ARG INCLUDE_LOCAL_MODELS=true
# =============================================================================
# Stage: API Builder
# =============================================================================
FROM python:3.11-slim AS api-builder
ARG INCLUDE_API
ARG INCLUDE_LOCAL_MODELS
RUN if [ "$INCLUDE_API" != "true" ]; then echo "Skipping API build" && exit 0; fi
WORKDIR /app
# Install system dependencies and uv
RUN apt-get update && apt-get install -y \
gcc \
g++ \
curl \
&& rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir uv
# Copy the workspace lock and member metadata before source code so dependency
# installation stays cacheable while matching the versions tested in CI.
COPY pyproject.toml uv.lock ./
COPY hindsight-all/pyproject.toml ./hindsight-all/
COPY hindsight-api/pyproject.toml ./hindsight-api/
COPY hindsight-api-slim/pyproject.toml ./api/
COPY hindsight-api-slim/README.md ./api/
COPY hindsight-all-slim/pyproject.toml ./hindsight-all-slim/
COPY hindsight-dev/pyproject.toml ./hindsight-dev/
COPY hindsight-clients/python/pyproject.toml ./hindsight-clients/python/
COPY hindsight-embed/pyproject.toml ./hindsight-embed/
RUN ln -s api hindsight-api-slim
# Sync dependencies using appropriate extras based on INCLUDE_LOCAL_MODELS
# local-ml: torch, sentence-transformers, transformers, einops, flashrank, mlx (optional)
# embedded-db: pg0-embedded (always included for embedded PostgreSQL support)
# ONNX Runtime embeddings are intentionally not bundled into the official
# standalone image; install the local-onnx extra in custom images when needed.
ENV UV_PROJECT_ENVIRONMENT=/app/api/.venv
RUN if [ "$INCLUDE_LOCAL_MODELS" = "true" ]; then \
uv sync --locked --package hindsight-api-slim --no-install-package hindsight-api-slim --extra local-ml --extra embedded-db; \
else \
uv sync --locked --package hindsight-api-slim --no-install-package hindsight-api-slim --extra embedded-db; \
fi
# Copy source code (alembic migrations are inside hindsight_api/)
WORKDIR /app/api
COPY hindsight-api-slim/hindsight_api ./hindsight_api
# Install the local package from the same validated lock after source is present.
WORKDIR /app
RUN if [ "$INCLUDE_LOCAL_MODELS" = "true" ]; then \
uv sync --locked --package hindsight-api-slim --extra local-ml --extra embedded-db; \
else \
uv sync --locked --package hindsight-api-slim --extra embedded-db; \
fi \
&& uv pip check --python /app/api/.venv/bin/python
# =============================================================================
# Stage: SDK Builder (needed for Control Plane)
# =============================================================================
FROM node:24-slim AS sdk-builder
ARG INCLUDE_CP
RUN if [ "$INCLUDE_CP" != "true" ]; then echo "Skipping SDK build" && exit 0; fi
WORKDIR /app
# Copy root package files for npm workspaces
COPY package.json package-lock.json ./
COPY hindsight-clients/typescript/ ./hindsight-clients/typescript/
# Install and build SDK using workspace (--ignore-scripts skips git hooks setup)
RUN npm ci --ignore-scripts -w @vectorize-io/hindsight-client
RUN npm run build -w @vectorize-io/hindsight-client
# =============================================================================
# Stage: Control Plane Builder
# =============================================================================
FROM node:24-slim AS cp-builder
ARG INCLUDE_CP
RUN if [ "$INCLUDE_CP" != "true" ]; then echo "Skipping CP build" && exit 0; fi
# Create directory structure matching the monorepo layout
# This is required because build:standalone script expects .next/standalone/memory-poc/hindsight-control-plane
WORKDIR /app/memory-poc/hindsight-control-plane
# Install Control Plane dependencies
# Only copy package.json (not package-lock.json) to ensure npm installs
# correct platform-specific native bindings for lightningcss/tailwindcss
COPY hindsight-control-plane/package.json ./
# Remove the file: dependency on SDK (we'll copy it directly later)
RUN sed -i '/"@vectorize-io\/hindsight-client":/d' package.json
RUN npm install
# Copy Control Plane source (excluding node_modules via .dockerignore)
COPY hindsight-control-plane/ ./
# Remove package-lock.json to avoid conflicts with installed native bindings
# Also remove the file: dependency from package.json (restored by COPY above)
RUN rm -f package-lock.json && sed -i '/"@vectorize-io\/hindsight-client":/d' package.json
# Copy built SDK directly into node_modules (more reliable than npm link in Docker)
COPY --from=sdk-builder /app/hindsight-clients/typescript ./node_modules/@vectorize-io/hindsight-client
# Accept base path as build argument for reverse proxy deployments
# Usage: docker build --build-arg NEXT_PUBLIC_BASE_PATH=/hindsight ...
ARG NEXT_PUBLIC_BASE_PATH=""
# Build Control Plane - run next build first, then custom standalone copy
# (The build:standalone script expects a specific path structure that differs in Docker)
RUN npm exec -- next build
# Create standalone directory structure manually
# Note: Must exclude node_modules from find to avoid wrong server.js from next/dist/experimental/testmode/
# Note: Must explicitly copy .next since glob * doesn't match hidden directories
RUN STANDALONE_ROOT=$(find .next/standalone -path '*/node_modules' -prune -o -name 'server.js' -print | head -1 | xargs dirname) && \
mkdir -p standalone && \
cp -r "$STANDALONE_ROOT"/* standalone/ && \
cp -r "$STANDALONE_ROOT"/.next standalone/.next && \
# Copy node_modules if separate from app dir (monorepo structure)
if [ -d ".next/standalone/node_modules" ] && [ "$STANDALONE_ROOT" != ".next/standalone" ]; then \
cp -r .next/standalone/node_modules standalone/node_modules; \
fi && \
cp -r .next/static standalone/.next/static && \
mkdir -p standalone/public && \
cp -r public/* standalone/public/ 2>/dev/null || true && \
# Verify required files exist
test -f standalone/server.js || (echo "ERROR: server.js missing!" && exit 1) && \
test -f standalone/.next/BUILD_ID || (echo "ERROR: BUILD_ID missing!" && exit 1)
# =============================================================================
# Stage: Final Image - API Only
# =============================================================================
FROM python:3.11-slim AS api-only
WORKDIR /app
# Note: libicu version varies by Debian version - try common versions in order
# Runtime images use uv directly; remove pip build tooling after installation so
# vulnerable setuptools-vendored packages and wheel are not shipped in production.
# `upgrade` before `install`: without it the image ships whatever package
# snapshot the base image was cut with, so security updates published since
# never reach the runtime layers - that is how 0.9.2 shipped openssl 3.5.6 with
# 3.5.7 available (#3906). The tradeoff is deliberate: two builds of the same
# commit can now resolve different package versions.
RUN apt-get update && apt-get upgrade -y \
&& apt-get install -y \
procps \
libssl3 \
libgssapi-krb5-2 \
libossp-uuid16 \
&& (apt-get install -y libicu72 2>/dev/null || apt-get install -y libicu74 2>/dev/null || apt-get install -y libicu76 2>/dev/null || true) \
&& rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir uv \
&& pip uninstall --yes setuptools wheel
RUN useradd -m -s /bin/bash hindsight
# Copy API with virtual environment from builder
COPY --chown=hindsight:hindsight --from=api-builder /app/api /app/api
# Copy startup script
COPY --chown=hindsight:hindsight docker/standalone/start-all.sh /app/start-all.sh
RUN chmod +x /app/start-all.sh
USER hindsight
# Create pg0 data directory as hindsight user so that Docker seeds new named
# volumes with correct ownership (UID 1000) on first use, avoiding the
# "Permission denied" error when mounting a fresh root-owned volume.
RUN mkdir -p /home/hindsight/.pg0
# Make /home/hindsight traversable when running with --user UID:GID overrides
# (default 0700 blocks traversal by non-owner UIDs needed for bind-mount ownership matching)
RUN chmod 755 /home/hindsight
ENV PATH="/app/api/.venv/bin:${PATH}"
# No tokenizer pre-download step: toktok compiles its vocabularies into the wheel,
# so token counting works offline with nothing cached at build time.
# Pre-download ML models to avoid runtime download (conditional)
# Only runs if both PRELOAD_ML_MODELS=true AND INCLUDE_LOCAL_MODELS=true
# Includes retry logic with exponential backoff for transient network failures
ARG PRELOAD_ML_MODELS
ARG INCLUDE_LOCAL_MODELS
ENV HF_HUB_DOWNLOAD_TIMEOUT=600
RUN if [ "$PRELOAD_ML_MODELS" = "true" ] && [ "$INCLUDE_LOCAL_MODELS" = "true" ]; then \
MAX_RETRIES=3; \
RETRY_DELAY=10; \
for i in $(seq 1 $MAX_RETRIES); do \
echo "Attempt $i/$MAX_RETRIES: Downloading ML models..."; \
/app/api/.venv/bin/python -c "\
import os; os.environ['HF_HUB_DOWNLOAD_TIMEOUT'] = '600'; \
from sentence_transformers import SentenceTransformer, CrossEncoder; \
print('Downloading embedding model...'); \
SentenceTransformer('BAAI/bge-small-en-v1.5'); \
print('Downloading cross-encoder model...'); \
CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2'); \
print('Models cached successfully')" && break; \
if [ $i -lt $MAX_RETRIES ]; then \
echo "Attempt $i failed, retrying in ${RETRY_DELAY}s..."; \
sleep $RETRY_DELAY; \
RETRY_DELAY=$((RETRY_DELAY * 2)); \
fi; \
done; \
if [ $i -eq $MAX_RETRIES ] && ! /app/api/.venv/bin/python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('BAAI/bge-small-en-v1.5')" 2>/dev/null; then \
echo "ERROR: Failed to download models after $MAX_RETRIES attempts"; \
exit 1; \
fi; \
elif [ "$INCLUDE_LOCAL_MODELS" != "true" ]; then echo "Skipping ML model preload (local-models not included)"; \
else echo "Skipping ML model preload"; fi
EXPOSE 8888
ENV HINDSIGHT_API_HOST=0.0.0.0
ENV HINDSIGHT_API_PORT=8888
ENV HINDSIGHT_API_LOG_LEVEL=info
ENV HINDSIGHT_ENABLE_API=true
ENV HINDSIGHT_ENABLE_CP=false
ENV PYTHONUNBUFFERED=1
# Suppress verbose transformers/HuggingFace model loading warnings
ENV TRANSFORMERS_VERBOSITY=error
ENV HF_HUB_VERBOSITY=error
ENV TOKENIZERS_PARALLELISM=false
CMD ["/app/start-all.sh"]
# =============================================================================
# Stage: Final Image - Control Plane Only
# =============================================================================
FROM node:24-alpine AS cp-only
WORKDIR /app
# Copy built SDK
COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
# Copy Control Plane standalone build
WORKDIR /app/control-plane
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/standalone ./
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/.next/static ./.next/static
COPY --from=cp-builder /app/memory-poc/hindsight-control-plane/public ./public
WORKDIR /app
# Copy startup script
COPY docker/standalone/start-all.sh /app/start-all.sh
RUN chmod +x /app/start-all.sh
# Install bash for the shared startup script. No curl: this image performs no
# health check. start-all.sh probes only the API (gated on ENABLE_API, which
# this stage sets to false) and the LLM under the opt-in HINDSIGHT_WAIT_FOR_DEPS,
# which a control-plane-only container has no reason to set; the control-plane
# branch launches `node server.js` and waits on nothing. There is no HEALTHCHECK
# instruction either. `apk upgrade` runs first for the same reason the Debian
# stages run `apt-get upgrade` - see the comment on the api-only stage.
# npm is removed for the same reason the Debian stages remove pip's build
# tooling: nothing invokes it at runtime - start-all.sh launches the pre-built
# Next standalone bundle with `node server.js` - and dropping it leaves no
# scannable vulnerable tar component in the stage. corepack is deliberately
# kept: it has no node_modules tree and ships no tar package metadata, so a
# scanner reports nothing for it. The node base image unpacks npm from its own
# tarball rather than installing an apk package, so it has to go by path.
# The `[ -e ]` guard is deliberate: a bare `rm -rf` on a path a future base
# image no longer uses succeeds silently and npm quietly ships again. Fail the
# build instead, so the path assumption has to be revisited on a base bump.
RUN apk upgrade --no-cache && apk add --no-cache bash \
&& [ -e /usr/local/lib/node_modules/npm ] \
&& rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx \
&& ! command -v npm
EXPOSE 9999
ENV NODE_ENV=production
ENV HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888
ENV HINDSIGHT_ENABLE_API=false
ENV HINDSIGHT_ENABLE_CP=true
CMD ["/app/start-all.sh"]
# =============================================================================
# Stage: Final Image - Standalone (both API and Control Plane)
# =============================================================================
FROM python:3.11-slim AS standalone
WORKDIR /app
# Install uv and system dependencies. Node arrives separately, below.
# Note: libicu version varies by Debian version - try common versions in order
# Runtime images use uv directly; remove pip build tooling after installation so
# vulnerable setuptools-vendored packages and wheel are not shipped in production.
# `upgrade` before `install`: without it the image ships whatever package
# snapshot the base image was cut with, so security updates published since
# never reach the runtime layers - that is how 0.9.2 shipped openssl 3.5.6 with
# 3.5.7 available (#3906). The tradeoff is deliberate: two builds of the same
# commit can now resolve different package versions.
RUN apt-get update && apt-get upgrade -y \
&& apt-get install -y \
procps \
libssl3 \
libgssapi-krb5-2 \
libossp-uuid16 \
&& (apt-get install -y libicu72 2>/dev/null || apt-get install -y libicu74 2>/dev/null || apt-get install -y libicu76 2>/dev/null || true) \
&& rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir uv \
&& pip uninstall --yes setuptools wheel
# This is a Python base image, but it also runs the control plane - a pre-built
# Next standalone bundle launched with `node server.js` - so it needs a Node
# runtime. Take cp-builder's binary rather than NodeSource's apt repo. That
# bootstrap was a `curl | bash` from a third-party host at build time, it made
# builds non-reproducible (the remote script and repo move under a fixed
# Dockerfile), and it dragged ~14 packages into the runtime image that nothing
# runs: the whole GnuPG suite, used only to verify the repo key at build time,
# plus a second system Python (3.13, in a python:3.11 image).
#
# Copying the binary also means npm never arrives, so the previous removal by
# path - and its `[ -e ]` guard against a base bump moving that path - is gone
# with it. corepack does not come along either; nothing invoked it.
#
# node:24-slim and python:3.11-slim are both Debian and glibc is backward
# compatible, so the binary runs here. `node --version` fails the build if a
# base change on either side ever breaks that, rather than shipping an image
# whose control plane cannot start.
COPY --from=cp-builder /usr/local/bin/node /usr/local/bin/node
COPY --from=cp-builder /usr/local/LICENSE /usr/local/LICENSE
RUN node --version && ! command -v npm
RUN useradd -m -s /bin/bash hindsight
# Copy API with virtual environment from builder
COPY --chown=hindsight:hindsight --from=api-builder /app/api /app/api
# Copy built SDK
COPY --chown=hindsight:hindsight --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
# Copy Control Plane standalone build
WORKDIR /app/control-plane
COPY --chown=hindsight:hindsight --from=cp-builder /app/memory-poc/hindsight-control-plane/standalone ./
COPY --chown=hindsight:hindsight --from=cp-builder /app/memory-poc/hindsight-control-plane/.next/static ./.next/static
COPY --chown=hindsight:hindsight --from=cp-builder /app/memory-poc/hindsight-control-plane/public ./public
WORKDIR /app
# Copy startup script
COPY --chown=hindsight:hindsight docker/standalone/start-all.sh /app/start-all.sh
RUN chmod +x /app/start-all.sh
USER hindsight
# Create pg0 data directory as hindsight user so that Docker seeds new named
# volumes with correct ownership (UID 1000) on first use, avoiding the
# "Permission denied" error when mounting a fresh root-owned volume.
RUN mkdir -p /home/hindsight/.pg0
# Make /home/hindsight traversable when running with --user UID:GID overrides
# (default 0700 blocks traversal by non-owner UIDs needed for bind-mount ownership matching)
RUN chmod 755 /home/hindsight
ENV PATH="/app/api/.venv/bin:${PATH}"
# No tokenizer pre-download step: toktok compiles its vocabularies into the wheel,
# so token counting works offline with nothing cached at build time.
# Pre-download ML models to avoid runtime download (conditional)
# Only runs if both PRELOAD_ML_MODELS=true AND INCLUDE_LOCAL_MODELS=true
# Includes retry logic with exponential backoff for transient network failures
ARG PRELOAD_ML_MODELS
ARG INCLUDE_LOCAL_MODELS
ENV HF_HUB_DOWNLOAD_TIMEOUT=600
RUN if [ "$PRELOAD_ML_MODELS" = "true" ] && [ "$INCLUDE_LOCAL_MODELS" = "true" ]; then \
MAX_RETRIES=3; \
RETRY_DELAY=10; \
for i in $(seq 1 $MAX_RETRIES); do \
echo "Attempt $i/$MAX_RETRIES: Downloading ML models..."; \
/app/api/.venv/bin/python -c "\
import os; os.environ['HF_HUB_DOWNLOAD_TIMEOUT'] = '600'; \
from sentence_transformers import SentenceTransformer, CrossEncoder; \
print('Downloading embedding model...'); \
SentenceTransformer('BAAI/bge-small-en-v1.5'); \
print('Downloading cross-encoder model...'); \
CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2'); \
print('Models cached successfully')" && break; \
if [ $i -lt $MAX_RETRIES ]; then \
echo "Attempt $i failed, retrying in ${RETRY_DELAY}s..."; \
sleep $RETRY_DELAY; \
RETRY_DELAY=$((RETRY_DELAY * 2)); \
fi; \
done; \
if [ $i -eq $MAX_RETRIES ] && ! /app/api/.venv/bin/python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('BAAI/bge-small-en-v1.5')" 2>/dev/null; then \
echo "ERROR: Failed to download models after $MAX_RETRIES attempts"; \
exit 1; \
fi; \
elif [ "$INCLUDE_LOCAL_MODELS" != "true" ]; then echo "Skipping ML model preload (local-models not included)"; \
else echo "Skipping ML model preload"; fi
EXPOSE 8888 9999
ENV HINDSIGHT_API_HOST=0.0.0.0
ENV HINDSIGHT_API_PORT=8888
ENV HINDSIGHT_API_LOG_LEVEL=info
ENV NODE_ENV=production
ENV HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888
ENV HINDSIGHT_ENABLE_API=true
ENV HINDSIGHT_ENABLE_CP=true
ENV PYTHONUNBUFFERED=1
# Suppress verbose transformers/HuggingFace model loading warnings
ENV TRANSFORMERS_VERBOSITY=error
ENV HF_HUB_VERBOSITY=error
ENV TOKENIZERS_PARALLELISM=false
CMD ["/app/start-all.sh"]
# =============================================================================
# Default target selection based on build args
# =============================================================================
FROM standalone AS default-both
FROM api-only AS default-api
FROM cp-only AS default-cp
# This selects the final stage based on INCLUDE_API and INCLUDE_CP
# Use --target to override: docker build --target api-only .
FROM standalone