# Python comes from uv, not from a `python:*-slim` base: that image's SQLite is
# Debian's system library, which lacks the WAL-reset corruption fix
# (template#829), while a uv-managed CPython bundles a current SQLite.  It also
# keeps the shipped interpreter off Renovate's Docker-tag bumps (template#828):
# keep PYTHON_VERSION below equal to the Python version whose ci.yml test job
# collects coverage, so the image runs the interpreter CI measures.
FROM debian:trixie-slim

# What a `python:*-slim` image carried and a bare Debian lacks: CA roots for
# TLS (git over HTTPS, outbound clients), /etc/services, and zoneinfo data.
# Plus tini, the init the ENTRYPOINT below runs as PID 1.
RUN apt-get update && apt-get install -y --no-install-recommends \
        ca-certificates netbase tini tzdata \
    && rm -rf /var/lib/apt/lists/*

# Here, between the two apt RUNs, so neither merges into the other: the
# packages above must not depend on what a project keeps in the block below.
COPY --from=ghcr.io/astral-sh/uv:0.13 /uv /uvx /bin/

# DOCKERFILE-APT-DEPS-START — add domain apt packages below; kept across copier update
RUN apt-get update && apt-get install -y --no-install-recommends git git-lfs gosu \
    && rm -rf /var/lib/apt/lists/* \
    && git lfs install --system
# DOCKERFILE-APT-DEPS-END

ARG PYTHON_VERSION=3.14
# The interpreter lives under /opt, not root's home, so the venv's symlinked
# `python` stays readable after the entrypoint drops to appuser.
# `only-managed` keeps `uv sync` off any python3 an apt package pulls in.
ENV LANG=C.UTF-8 \
    UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy \
    UV_PYTHON=${PYTHON_VERSION} \
    UV_PYTHON_INSTALL_DIR=/opt/python \
    UV_PYTHON_PREFERENCE=only-managed
RUN uv python install "$PYTHON_VERSION"

WORKDIR /app

# Optional remote-debugger listener.  Default off so production images stay
# lean; pass ``--build-arg DEBUG=true`` to install the ``[debug]`` extra
# (debugpy) and bake the listener into the image.  Accepts the same boolean
# vocabulary as runtime ``parse_bool`` (``true``/``1``/``yes``/``on``,
# case-insensitive); anything else is treated as off.  See
# ``docs/deploy/docker.md`` for the full attach workflow.
ARG DEBUG=false

# DOCKERFILE-UV-EXTRAS-START — append `--extra <name>` flags below to pull domain-specific extras; kept across copier update
# Install dependencies first (cache layer).  The ``$( ... )`` shell
# expansion appends ``--extra debug`` only when ``--build-arg DEBUG=true``;
# default builds get the lean install.
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    uv sync --frozen --no-install-project --no-dev --extra all \
        $( case "$DEBUG" in [Tt][Rr][Uu][Ee]|1|[Yy][Ee][Ss]|[Oo][Nn]) echo "--extra debug" ;; esac )

# Copy source and install project.
COPY pyproject.toml uv.lock README.md /app/
COPY src/ /app/src/
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev --extra all \
        $( case "$DEBUG" in [Tt][Rr][Uu][Ee]|1|[Yy][Ee][Ss]|[Oo][Nn]) echo "--extra debug" ;; esac )
# DOCKERFILE-UV-EXTRAS-END

# Create non-root user with configurable UID/GID for bind-mount compatibility.
ARG APP_UID=1000
ARG APP_GID=1000
RUN if [ "$APP_UID" -eq 0 ] || [ "$APP_GID" -eq 0 ]; then \
        echo "ERROR: APP_UID and APP_GID must be non-zero" >&2; exit 1; \
    fi \
    && groupadd -r --gid $APP_GID --non-unique appuser \
    && useradd -r --uid $APP_UID --gid $APP_GID --no-log-init -d /app appuser \
    # DOCKERFILE-STATE-DIRS-START — domain state subdirs; kept across copier update
    && mkdir -p /data/vault /data/state/embeddings /data/state/fastembed /data/state/fastmcp \
    # DOCKERFILE-STATE-DIRS-END
    && chown -R appuser:appuser /app /data

COPY --chmod=0755 docker-entrypoint.sh /usr/local/bin/
# No log-format default on purpose: pvl-core renders one JSON object per
# record whenever stderr is not a terminal, which a container's never is, so
# `docker logs` and a collector get parseable lines with nothing set here.
# `MARKDOWN_VAULT_MCP_LOG_FORMAT=rich` in `.env` forces the colour renderer
# back for a person reading the stream; pinning either value in the image
# would take that choice away from the deployment.
ENV PATH="/app/.venv/bin:$PATH" \
    FASTMCP_HOME=/data/state/fastmcp

EXPOSE 8000
# Remote debugger: ``EXPOSE`` is metadata — nothing actually listens unless
# the image was built with ``--build-arg DEBUG=true`` (which installs
# debugpy) AND ``MARKDOWN_VAULT_MCP_DEBUG_PORT`` is set at runtime.  Always
# declared so the toggled ``[debug]`` extra has a stable port surface
# to reach with ``-p 127.0.0.1:5678:5678``.
EXPOSE 5678

# DOCKERFILE-VOLUMES-START — mounted volume list; kept across copier update
VOLUME ["/data/vault", "/data/state"]
# DOCKERFILE-VOLUMES-END

# tini is PID 1 and reaps every orphan re-parented to it.  The server would
# reap only the children it waits on, so a subprocess that detaches (git's
# `maintenance run --auto --detach`, for one) would stay a zombie until the
# container's pids limit stops every fork (template#840).  `-s` keeps tini
# reaping when it is not PID 1, under `docker run --init` or compose's
# `init: true`, instead of warning on every start.
ENTRYPOINT ["/usr/bin/tini", "-s", "--", "/usr/local/bin/docker-entrypoint.sh"]
# Liveness for a bare `docker run`: the same `GET /health` probe compose.yml
# declares, so a container reports health with or without Compose (a compose
# `healthcheck:` overrides this one where both exist).  Assumes the
# conventional `/mcp` mount — see the compose.yml comment for the
# `MARKDOWN_VAULT_MCP_HTTP_PATH` caveat.  Shell form on purpose: `python` is the
# venv interpreter on PATH, and the one-liner needs no argument splitting.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2).close()"
# Both the bind address and the port are fixed by the image, not read from the
# environment: `EXPOSE`, `HEALTHCHECK`, the compose port mapping and the compose
# healthcheck all name 8000, and `MARKDOWN_VAULT_MCP_PORT` in a `.env` would
# otherwise move the listener out from under all four.  Publish a different
# host port instead (`-p 9000:8000`, or compose's `ports:`).
CMD ["markdown-vault-mcp", "serve", "--transport", "http", "--host", "0.0.0.0", "--port", "8000"]
