# Built from the repository root, not from `backend/`: # # docker build -f backend/Dockerfile . # # because the image has to carry `emails/compiled/`, which lives beside `backend/` # rather than inside it. With `./backend` as the context the compose files had to # bind-mount the directory from a checkout, and an image with a bind mount it # cannot do without is an image nobody else can pull (#1545). `.dockerignore` at # the root says what of the tree reaches the daemon. # Build stage FROM python:3.15-slim AS builder ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 WORKDIR /app # Install uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv ENV UV_COMPILE_BYTECODE=1 ENV UV_LINK_MODE=copy # Optional extras to build into this image, as `uv sync` flags - empty by # default, so a plain build is exactly what it was. # # It exists because the capability documentation tells an operator to install # `agenticos[browser]` and, until this argument, nothing in the build honoured # that: the image was `--no-dev` with no extras, so there was no supported way to # get the engine in and a bound agent failed its one tool with an install line # nobody could act on. `browser_use` has had the same gap since it shipped. # # docker build --build-arg EXTRAS="--extra browser" ... # # Not a default, and not a list this file decides: an extra is a dependency tree # and a licence obligation, so which ones a deployment carries is the operator's # choice, made where they build. ARG EXTRAS="" # The `kerberos` extra is `gssapi`, which publishes no Linux wheel and builds # against the system Kerberos headers (#1773). Only a build that asks for it pays # for a compiler here; the runtime stage takes the library alone. RUN if echo "${EXTRAS}" | grep -q kerberos; then \ apt-get update && apt-get install -y --no-install-recommends gcc libkrb5-dev \ && apt-get clean && rm -rf /var/lib/apt/lists/*; \ fi RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=backend/uv.lock,target=uv.lock \ --mount=type=bind,source=backend/pyproject.toml,target=pyproject.toml \ uv sync --frozen --no-install-project --no-dev ${EXTRAS} # Copy application COPY backend /app COPY emails /app/emails # Install project RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev ${EXTRAS} # The application's own modules, compiled once here. `UV_COMPILE_BYTECODE` # covers the dependencies only, and the runtime sets `PYTHONDONTWRITEBYTECODE`, # so every process - each API worker, the worker, every ingest child it # respawns, every migration - compiled all of `app/` at start and threw the # result away (#2229). RUN python -m compileall -q -j 0 app cli alembic # Runtime stage FROM python:3.15-slim ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 WORKDIR /app # LiteParse converts non-PDF documents to PDF before parsing, and the two # conversion paths need different things present: # # - office formats (.docx, .xlsx, .pptx, .odt, .csv, ...) shell out to # LibreOffice. Without it, uploading a spreadsheet to a LiteParse collection # fails at conversion — so it is installed rather than left to the operator. # `libreoffice-core` + the three filters is roughly a third of the full # `libreoffice` package and covers every format we advertise. # - images (.png, .jpg, .tiff, ...) are converted by liteparse's own Rust # code and need nothing installed. # # `tesseract-ocr` is the OCR engine behind `ocr: true`; the language packs are # what `ocr_language` selects between, and a code with no pack installed is a # parse that returns nothing. English and Polish are shipped — add # `tesseract-ocr-` here for others. # # No Node.js: the `liteparse` PyPI package ships its own native binary (PyO3). # The npm CLI this used to install was never on the code path. # # `git` is the Git sync connector's transport (#987): it clones a repository's # documentation sparse and partial, which no Python client does as cheaply. # `ca-certificates` is what it verifies the host's certificate against. RUN apt-get update && apt-get install -y --no-install-recommends \ libreoffice-core libreoffice-writer libreoffice-calc libreoffice-impress \ tesseract-ocr tesseract-ocr-eng tesseract-ocr-pol \ git ca-certificates \ && apt-get clean && rm -rf /var/lib/apt/lists/* # What `gssapi` loads at run time, for an image built with the `kerberos` extra. # Redeclared because a build argument does not cross a stage boundary. ARG EXTRAS="" RUN if echo "${EXTRAS}" | grep -q kerberos; then \ apt-get update && apt-get install -y --no-install-recommends libgssapi-krb5-2 \ && apt-get clean && rm -rf /var/lib/apt/lists/*; \ fi # Application and its virtual environment, from the builder COPY --from=builder /app /app # The licence, the notice and the generated index of every third-party component # in this image (docs/licenses.md). The wheels' own licence files are already in # `.venv/**/*.dist-info/`; this is what tells a reader of the image which is which. # The texts directory is for the wheels that publish no licence file of their own: # their `METADATA` names the licence and the author, and this is the text. It lives # under `frontend/` because the frontend image's build context cannot reach the # repository root, and one copy is better than two. COPY LICENSE NOTICE THIRD_PARTY_NOTICES.md /app/ COPY frontend/licenses/texts /app/licenses/texts # Add venv to path ENV PATH="/app/.venv/bin:$PATH" # Non-root user. Code and venv stay root-owned and read-only to it; only the # directories it writes at run time are its own. uid pinned: media_data volumes # created by earlier images are owned by 1000. RUN adduser --disabled-password --gecos "" --uid 1000 appuser && \ mkdir -p /app/media /app/data /app/chroma_data /app/models_cache && \ chown -R appuser:appuser /app/media /app/data /app/chroma_data /app/models_cache USER appuser EXPOSE 8000 # No HEALTHCHECK here, deliberately. This image has two consumers with different # commands: the API (`uvicorn app.main:app`) and the background worker # (`python -m app.worker`). The probe that used to live here asked # localhost:8000 for `/api/v1/health`, which the worker serves nothing on - so the # worker was `unhealthy` from the second it started and stayed that way, hiding # the one thing a health status exists to show. An image cannot know which of the # two it was started as; a service definition does. Both probes are in the compose # files, one per service. CMD ["python", "-m", "cli.commands", "server", "run", "--host", "0.0.0.0", "--port", "8000"]