Ctrl K
ComunidadGuías…

backend/Dockerfile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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-<lang>` 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"]