# MESH_SILOS — Dockerfile for production deployment.
#
# OPTIONAL. The product does not require Docker: `pip install
# mesh-silos[all]` is the documented install and a container is one
# packaging option among several. Docker was decoupled from the project in
# v0.8.51 — `pyproject.toml` mentions it nowhere, and the "the shipped
# artefact serves the UI" check runs against the installed wheel
# (`tools/_verify_wheel_install.py`), not a container.
#
# What DOES read this file, and is worth stating because an earlier version of
# this comment claimed nothing did `[M]`: `make dockerfile`, and the
# `dockerfile` step of `tools/ci.py all` (`Makefile:150-152`,
# `tools/ci.py::step_dockerfile`, in `ALL_STEPS`). Both run
# `tools/_verify_dockerfile.py`, which parses this file as **text** — it checks
# that every COPY source exists in the tree, that the install precedes the
# checks, and that the CMD resolves. It never contacts a daemon and never builds
# an image, so a machine with no container runtime still runs it and still gets
# a verdict. What that step proves is that this file is *consistent with the
# tree*; what it does not prove is that the image builds, and no gate here can
# claim otherwise without a daemon.
#
# Keep this file if you deploy to a registry; ignore it if you run from a wheel.
#
# Multi-stage build:
#   1. ``builder`` installs the Python dependencies and the FastAPI app.
#   2. ``runtime`` is a slim image with only what the orchestrator needs.
#
# The container is air-gapped by default: outbound sockets are blocked
# at the Python interpreter level. The operator can flip
# ``MESH_ALLOW_NETWORK=1`` for *test* deployments only.
#
# Build:    docker build -t mesh_silos:local .
# Run:      docker run --rm -p 8000:8000 -v $PWD/out:/app/out mesh_silos:local
# Verify:   docker run --rm -v $PWD/out:/app/out mesh_silos:local python -m mesh_silos verify --out-dir /app/out
#
# The tag is deliberately not versioned. These examples said `0.8.26` while
# the project was at 0.8.38, and would have said so again at the next
# release: a copyable command that names a version nobody updates is worse
# than one that names none. Tag with the version from ``pyproject.toml``
# if you want a versioned image for a registry.
#
# Two things worth knowing before you build this:
#
#   * ``MESH_API_SECRET=""`` below is NOT a hardcoded secret. An empty
#     value makes ``mesh_silos/backend/auth.py`` generate a random 32-byte secret
#     for the lifetime of the process. That is safe but it means every
#     restart invalidates live tokens; set a real one for anything that
#     has to survive a bounce.
#   * The web UI is **not** built here. Node is not in this image by
#     design. Run ``python scripts/build_ui.py`` on the host before
#     ``docker build`` so the staged export lands in ``mesh_silos/frontend/out``
#     and ships as package data; the launcher then mounts it at ``/``.
#
# [unverified] These commands were NOT executed when this file was last
# edited: no Docker daemon is available in the authoring environment. What
# *was* verified is the premise behind the ``COPY data/`` removal below --
# ``.gitignore:32`` excludes ``data/`` and ``git ls-files data`` is empty --
# so a fresh clone has no ``data/`` directory to copy. Please run the Build
# line above once on a clean clone before trusting it.

FROM python:3.11-slim AS builder

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# Install build deps for cryptography (Ed25519 signer).
RUN apt-get update -qq && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

COPY pyproject.toml ./
COPY mesh_silos/ ./mesh_silos/
# REMOVED in v0.8.67: `COPY backend/ ./backend/` used to sit here, added when
# the build first died with "package directory 'backend' does not exist". That
# was true while `backend/` was a top-level package. v0.8.65 moved it to
# `mesh_silos/backend/` and dropped it from `[tool.setuptools] packages`, and
# this line stayed — so from v0.8.65 onward the build failed at this
# instruction with "COPY failed: file not found in build context", on a Docker
# host, in a file that can only be verified by running it. Nothing here had
# run since. `tools/_verify_dockerfile.py` now checks every COPY source against
# the tree, so a move that orphans a COPY fails a gate instead of a build.
COPY scripts/ ./scripts/
COPY tools/ ./tools/
COPY tests/ ./tests/
# `frontend/` carries sources and the lockfile. `.dockerignore` keeps
# `node_modules`, `.next` and `out` out of the context, so this is a few
# hundred KB -- but without it the build-time suite fails on
# FileNotFoundError for `frontend/lib/api.ts` and friends, because several
# tests read the frontend sources to check that they point at the right
# API prefix. `frontend/out` is deliberately absent from the context;
# tests that compare against it skip rather than fail, since a build
# context is not a checkout.
COPY frontend/ ./frontend/
COPY docs/ ./docs/
# No `COPY data/ ./data/`: `.gitignore` excludes `data/` (it is user data,
# never committed), so `git ls-files data` is empty and the directory does
# not exist in a fresh clone. That COPY made `docker build` fail outright for
# anyone who had not run the tool locally first. Nothing in the image needs
# it -- the runtime stage never reads ./data, and the orchestrator creates
# its own dirs. Mount your real data with -v instead (see the header).
COPY .coveragerc ./
COPY LICENSE README.md ./
# MANIFEST.in is what makes the sdist carry the staged UI at all;
# `tests/unit/test_ui_packaging.py` reads it and fails without it.
COPY MANIFEST.in ./

# The web UI is a build artefact that git never carries: `frontend/out/` is
# gitignored, so `COPY mesh_silos/ ./mesh_silos/` above brings whatever the host has
# staged and nothing at all otherwise. That failure mode is silent in the
# worst way -- the image builds, installs, starts, and serves an API with no
# interface, which is exactly what `tests/unit/test_ui_packaging.py` was
# written to catch and what made `pip install silos` "print API only" in
# the first place.
#
# So the build fails here instead. This calls the gate that already exists
# rather than adding a shell predicate: a `test -f ... || exit 1` line is
# one nobody can execute outside a container, whereas
# `scripts/build_ui.py --check` runs on any host with Python, is the same
# check the release process uses, and returns non-zero with a diagnosis.
#
# ORDERING (fixed v0.8.48): the dependencies are installed FIRST, below,
# because `build_ui.py --check` also runs `tools/_verify_csp.py`, which
# imports `fastapi.testclient` to serve the export. Running the check
# before the install made `docker build` fail with
# `ModuleNotFoundError: No module named 'fastapi'` — the gate could not
# pass, and an image had never actually been built from this file. The
# check now runs after the install and before the test tooling is removed.

# The dev extra exists for the build-time gate only: `.[all]` does NOT
# include pytest, so `python scripts/run_tests.py` would fall back to the
# stdlib subset and verify 650 of the suite while looking thorough.
# Verified, then removed so the runtime image ships no test tooling.
RUN pip install --upgrade pip && \
    pip install ".[all,dev]"

# Now that the app's dependencies exist, the staged-UI + CSP gate can run.
RUN python scripts/build_ui.py --check

RUN python scripts/run_tests.py && \
    pip uninstall -y pytest coverage httpx ruff pypdf

# The web UI is NOT built here -- Node is absent by design. Run
# `python scripts/build_ui.py` on the host first so the staged export
# lands in mesh_silos/frontend/out and ships as package data.


# ---------------------------------------------------------------------------
# Runtime — slim.
# ---------------------------------------------------------------------------

FROM python:3.11-slim AS runtime

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    MESH_ALLOW_NETWORK=0 \
    MESH_OUT_DIR=/app/out \
    MESH_API_SECRET=""

# Non-root user for the orchestrator process.
RUN useradd --create-home --shell /usr/sbin/nologin mesh_silos

COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY --from=builder /app/mesh_silos /app/mesh_silos
COPY --from=builder /app/scripts /app/scripts
COPY --from=builder /app/tools /app/tools
COPY --from=builder /app/pyproject.toml /app/pyproject.toml
COPY --from=builder /app/LICENSE /app/LICENSE
COPY --from=builder /app/README.md /app/README.md

RUN mkdir -p /app/out && chown -R mesh_silos:mesh_silos /app

USER mesh_silos
WORKDIR /app

EXPOSE 8000

# Default: serve the product — the REST surface AND the bundled UI at /.
#
# ``mesh_silos.serve:app`` mounts the staged Next.js export the same way the
# desktop launcher does, then exposes the FastAPI app. Pointing CMD at
# ``mesh_silos.backend.app:app`` instead served the API alone: the wheel shipped 112 UI
# files, the build checked them, and ``/`` returned 404, because the UI is
# mounted by the launcher and by nothing else. This is the one failure mode
# the whole staging step exists to prevent, so the container must not have it.
#
# Operators who want the CLI instead override CMD:
#   docker run --rm mesh_silos:local python -m mesh_silos verify --out-dir /app/out
CMD ["uvicorn", "mesh_silos.serve:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]