mirror of
https://github.com/vectorize-io/hindsight.git
synced 2026-09-14 19:31:49 +08:00
5f9bff9056
* 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.
448 lines
20 KiB
Docker
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
|