Dockerfile
Rama main · réplica de lectura del repositorio original.
# syntax=docker/dockerfile:1.9
# Stage 1: provide Rust toolchain (required by setup.py -> build_ov_cli_artifact -> cargo build)
# ragfs-python's default S3-enabled dependency set currently requires rustc >= 1.91.1.
FROM rust:1.91.1-trixie AS rust-toolchain
# Stage 2: build Studio separately so npm failures stop the Docker build.
FROM node:24-trixie-slim AS web-studio-builder
ARG TARGETPLATFORM
WORKDIR /app/web-studio
# Keep npm install cached when only Studio sources change.
COPY web-studio/package.json web-studio/package-lock.json ./
RUN --mount=type=cache,target=/root/.npm,id=npm-${TARGETPLATFORM} npm ci
COPY web-studio/ ./
RUN npm run build -- --base=/studio/ \
&& test -f dist/index.html
# Stage 3: build Python environment with uv (builds Rust CLI + C++ extension)
FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim AS py-builder
# Reuse Rust toolchain from stage 1 so setup.py can compile ov CLI in-place.
COPY --from=rust-toolchain /usr/local/cargo /usr/local/cargo
COPY --from=rust-toolchain /usr/local/rustup /usr/local/rustup
ENV CARGO_HOME=/usr/local/cargo
ENV RUSTUP_HOME=/usr/local/rustup
ENV PATH="/app/.venv/bin:/usr/local/cargo/bin:${PATH}"
ARG OPENVIKING_VERSION=
ARG TARGETPLATFORM
ARG UV_LOCK_STRATEGY=auto
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
ccache \
cmake \
git \
&& rm -rf /var/lib/apt/lists/*
# Route gcc/g++/cc through ccache so cmake (which asks shutil.which("gcc")) picks
# up /usr/lib/ccache/gcc and benefits from the BuildKit cache mount on /root/.ccache.
ENV PATH="/usr/lib/ccache:${PATH}"
ENV CCACHE_DIR=/root/.ccache
# Pin Cargo's target dir to a stable path so a BuildKit cache mount can persist
# build artifacts across layer reruns even when uv builds the wheel in an
# ephemeral isolated tempdir.
ENV CARGO_TARGET_DIR=/cargo-target
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
ENV UV_NO_DEV=1
ENV OPENVIKING_TREE_SITTER_CACHE_DIR=/app/.cache/tree-sitter-language-pack
WORKDIR /app
# Copy source required for setup.py artifact builds and native extension build.
COPY Cargo.toml Cargo.lock ./
COPY pyproject.toml uv.lock setup.py README.md ./
COPY scripts/build_support/ scripts/build_support/
COPY bot/ bot/
COPY crates/ crates/
COPY openviking/ openviking/
COPY --from=web-studio-builder /app/web-studio/dist/ openviking/web_studio/dist/
COPY openviking_cli/ openviking_cli/
COPY openviking_gateway/ openviking_gateway/
COPY src/ src/
COPY third_party/ third_party/
# Install project and dependencies. setup.py packages the copied Studio bundle
# without running npm again, while build_ext still builds native extensions.
# Default to auto-refreshing uv.lock inside the ephemeral build context when it is
# stale, so Docker builds stay unblocked after dependency changes. Set
# UV_LOCK_STRATEGY=locked to keep fail-fast reproducibility checks.
RUN --mount=type=cache,target=/root/.cache/uv,id=uv-${TARGETPLATFORM} \
--mount=type=cache,target=/cargo-target,id=cargo-target-${TARGETPLATFORM} \
--mount=type=cache,target=/usr/local/cargo/registry,id=cargo-registry-${TARGETPLATFORM} \
--mount=type=cache,target=/usr/local/cargo/git,id=cargo-git-${TARGETPLATFORM} \
--mount=type=cache,target=/root/.ccache,id=ccache-${TARGETPLATFORM} \
if [ -n "${OPENVIKING_VERSION:-}" ]; then \
export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENVIKING="${OPENVIKING_VERSION}"; \
elif [ -f openviking/_version.py ]; then \
export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENVIKING="$(python -c "import runpy; print(runpy.run_path('openviking/_version.py')['version'])")"; \
else \
echo "OPENVIKING_VERSION build arg is required when building without openviking/_version.py" >&2; \
exit 2; \
fi; \
case "${UV_LOCK_STRATEGY}" in \
locked) \
uv sync --locked --no-editable --reinstall-package openviking --extra bot --extra gemini --extra opengauss --extra expiry --extra mysql --extra redis-mysql --extra reporting \
;; \
auto) \
if ! uv lock --check; then \
uv lock; \
fi; \
uv sync --locked --no-editable --reinstall-package openviking --extra bot --extra gemini --extra opengauss --extra expiry --extra mysql --extra redis-mysql --extra reporting \
;; \
*) \
echo "Unsupported UV_LOCK_STRATEGY: ${UV_LOCK_STRATEGY}" >&2; \
exit 2 \
;; \
esac
# Preload every tree-sitter grammar at build time. The build network can reach
# GitHub, while production usually cannot, so the populated cache is copied into
# the runtime image and the first code parse never blocks on a download. Fail the
# build if any language is missing instead of shipping an incomplete cache.
RUN rm -rf "${OPENVIKING_TREE_SITTER_CACHE_DIR}" \
&& mkdir -p "${OPENVIKING_TREE_SITTER_CACHE_DIR}" \
&& /app/.venv/bin/python - <<'PY'
import os
from typing import get_args
from tree_sitter_language_pack import (
PackConfig,
SupportedLanguage,
configure,
download_all,
downloaded_languages,
)
cache_dir = os.environ["OPENVIKING_TREE_SITTER_CACHE_DIR"]
configure(PackConfig(cache_dir=cache_dir))
download_all()
missing = sorted(set(get_args(SupportedLanguage)) - set(downloaded_languages()))
if missing:
raise SystemExit(
"tree-sitter parser preload incomplete; missing "
f"{len(missing)} languages: {', '.join(missing[:20])}"
)
PY
# Stage 4: runtime
FROM python:3.13-slim-trixie
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git \
libstdc++6 \
libcairo2 \
&& rm -rf /var/lib/apt/lists/*
# Resolve relative storage paths inside the persistent mount.
WORKDIR /app/.openviking
COPY --from=py-builder /app/.venv /app/.venv
# Grammar cache preloaded in the builder stage; read directly at runtime.
COPY --from=py-builder /app/.cache/tree-sitter-language-pack /app/.cache/tree-sitter-language-pack
# Exercise SVG rendering so missing Python or Cairo runtime dependencies fail the build.
RUN /app/.venv/bin/python -I - <<'PYCODE'
import cairosvg
svg = b'<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"><rect width="8" height="8" fill="red"/></svg>'
png = cairosvg.svg2png(bytestring=svg)
assert png.startswith(b"\x89PNG\r\n\x1a\n"), "SVG conversion did not produce a PNG"
PYCODE
# Fail the image build if VikingBot and the separately released SDK drift apart.
RUN /app/.venv/bin/python -I -c "import inspect, typing; from importlib.metadata import version; from openviking_sdk.client import AsyncHTTPClient; from openviking_sdk.options import BatchWriteOptions; checks = {'get_skill.include_integrity': 'include_integrity' in inspect.signature(AsyncHTTPClient.get_skill).parameters, 'ls.offset': 'offset' in inspect.signature(AsyncHTTPClient.ls).parameters, 'grep.level_limit': 'level_limit' in inspect.signature(AsyncHTTPClient.grep).parameters, 'BatchWriteOptions.skip_conflicts': 'skip_conflicts' in typing.get_type_hints(BatchWriteOptions)}; missing = [name for name, ok in checks.items() if not ok]; raise SystemExit(f\"incompatible openviking-sdk {version('openviking-sdk')}: missing {', '.join(missing)}\" if missing else 0)"
RUN /app/.venv/bin/python -I -c "from importlib.util import find_spec; from pathlib import Path; spec = find_spec('openviking.web_studio'); locations = list(spec.submodule_search_locations or ()) if spec else []; root = Path('/app/.venv').resolve(); p = (Path(locations[0]).resolve() / 'dist/index.html').resolve() if len(locations) == 1 else None; valid = p is not None and p.is_file() and p.is_relative_to(root); raise SystemExit(0 if valid else f'missing or misplaced Studio bundle: spec_found={spec is not None}, locations={locations!r}, resource={p}')"
# Redis and the gateway Lua scripts must survive wheel packaging.
RUN /app/.venv/bin/python -I -c "import redis; from importlib.resources import files; assert any(p.name.endswith('.lua') for p in files('openviking_gateway').joinpath('lua').iterdir()), 'gateway Lua scripts missing from installed package'"
COPY deploy/docker/openviking-entrypoint.sh /usr/local/bin/openviking-entrypoint
COPY deploy/docker/pending_health_server.py /usr/local/bin/openviking-pending-health
RUN mkdir -p /app/.openviking \
&& sed -i 's/\r$//' /usr/local/bin/openviking-entrypoint /usr/local/bin/openviking-pending-health \
&& chmod +x /usr/local/bin/openviking-entrypoint /usr/local/bin/openviking-pending-health
ENV HOME="/app" \
PATH="/app/.venv/bin:$PATH" \
OPENVIKING_TREE_SITTER_CACHE_DIR="/app/.cache/tree-sitter-language-pack" \
OPENVIKING_CONFIG_FILE="/app/.openviking/ov.conf" \
OPENVIKING_CLI_CONFIG_FILE="/app/.openviking/ovcli.conf"
EXPOSE 1933
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD ["openviking-entrypoint", "--healthcheck"]
# Default persistent state (ov.conf, ovcli.conf, workspace data) lives under
# /app/.openviking, which mirrors the host's ~/.openviking layout. Mount one
# volume there to persist everything across container restarts:
# docker run -v ~/.openviking:/app/.openviking <image>
# If ov.conf is absent on first start, set OPENVIKING_CONF_CONTENT to the full
# JSON, or `docker exec` in and run `openviking-server init`.
# Override command to run CLI, e.g.:
# docker run --rm -v ~/.openviking:/app/.openviking <image> openviking --help
ENTRYPOINT ["openviking-entrypoint"]