# Canonical's maintained glibc-compatible Python runtime is kept as a named stage so the builder # can preserve its passwd/group contract without adding a shell or package manager to the final # image. FROM ubuntu/python:3.12-24.04@sha256:d5b4c1e1da458c6e10da9d7f42276d8d640ed5a0d87b1cced734a9434c531a62 AS runtime-base # The builder is deliberately separate from the runtime. The agent only needs the Python # application packages at runtime; a full Debian build environment must not become part of the # shipped image or its vulnerability surface. FROM python:3.12-slim-trixie@sha256:e5c9fa26ffb76e11e0f054f30dc2523a2f9693f0c36c0cf1e39b27e152d899fc AS builder ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 WORKDIR /build COPY config/security/build-toolchain.lock /tmp/modelforge-build-toolchain.lock COPY backend ./backend COPY node-agent ./node-agent # Install into a plain target directory so the final image can use Canonical's maintained, # glibc-compatible chiselled Python runtime. Hash-locked build tooling is removed inside the # builder only; no installer or build-backend metadata is carried into the runtime image. The # target install ignores the builder venv so a build-only package cannot mask a runtime dependency. # The import check exercises the same dependency closure that the final image receives without # contacting the control plane or touching a model repository. RUN python -m venv /opt/build-venv \ && /opt/build-venv/bin/python -m pip install --no-cache-dir --disable-pip-version-check \ --require-hashes --only-binary=:all: --index-url https://pypi.org/simple \ -r /tmp/modelforge-build-toolchain.lock \ && /opt/build-venv/bin/python -m pip install --no-cache-dir --disable-pip-version-check \ --no-build-isolation --ignore-installed --no-compile \ --target /opt/site-packages ./backend ./node-agent \ && PYTHONPATH=/opt/site-packages /opt/build-venv/bin/python -m pip check \ && PYTHONPATH=/opt/site-packages /opt/build-venv/bin/python -c \ "import httpx, psutil, pynvml, modelforge_api, modelforge_node_agent" \ && /opt/build-venv/bin/python -m pip uninstall --yes pip setuptools wheel editables hatchling pathspec \ pluggy tomlkit trove-classifiers # Prepare the fixed runtime contract without requiring a shell or package manager in the final # image. The numeric IDs are intentional: Compose and existing volumes rely on UID 100/GID 101. RUN mkdir -p /runtime/app /runtime/data/state /runtime/data/hf-cache \ /runtime/data/artifacts /runtime/data/quarantine \ && chown -R 100:101 /runtime/app /runtime/data # Preserve the canonical runtime's passwd/group entries and add the existing ModelForge identity # with its established home directory. The final image remains shell-less and runs numerically as # UID 100/GID 101, while libraries that resolve the process user still see the same contract. COPY --from=runtime-base /etc/passwd /runtime/etc/passwd COPY --from=runtime-base /etc/group /runtime/etc/group RUN python -c "from pathlib import Path; p=Path('/runtime/etc/passwd'); s=p.read_text(); p.write_text(s if '\\nmodelforge-agent:' in s else s.rstrip('\\n')+'\\nmodelforge-agent:x:100:101::/app:/usr/sbin/nologin\\n'); p=Path('/runtime/etc/group'); s=p.read_text(); p.write_text(s if '\\nmodelforge-agent:' in s else s.rstrip('\\n')+'\\nmodelforge-agent:x:101:\\n')" FROM runtime-base ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PYTHONPATH=/opt/site-packages \ HOME=/app # Build identity. These are stamped in at build time so a running container can say exactly # where it came from; an argument that is never passed stays empty and is reported as null # rather than becoming a claimed commit. ARG MODELFORGE_VERSION=0.0.0 ARG MODELFORGE_COMMIT="" ARG MODELFORGE_BUILT_AT="" ENV MODELFORGE_BUILD_COMMIT=${MODELFORGE_COMMIT} ENV MODELFORGE_BUILD_TIMESTAMP=${MODELFORGE_BUILT_AT} LABEL org.opencontainers.image.title="ITWorx ModelForge node agent" LABEL org.opencontainers.image.description="Outbound-only compute node agent for ITWorx ModelForge" LABEL org.opencontainers.image.version="${MODELFORGE_VERSION}" LABEL org.opencontainers.image.revision="${MODELFORGE_COMMIT}" LABEL org.opencontainers.image.created="${MODELFORGE_BUILT_AT}" LABEL org.opencontainers.image.source="https://git.example.com/example/modelforge" LABEL org.opencontainers.image.vendor="ITWorx" LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later" COPY --from=builder --chown=100:101 /opt/site-packages /opt/site-packages COPY --from=builder --chown=100:101 /runtime/app /app COPY --from=builder --chown=100:101 /runtime/data /data COPY --from=builder /runtime/etc/passwd /etc/passwd COPY --from=builder /runtime/etc/group /etc/group WORKDIR /app USER 100:101 # Canonical's rock image has a Pebble entrypoint. The agent has no init supervisor requirement, # so explicitly remove it and invoke the reviewed Python module directly. ENTRYPOINT [] CMD ["python3", "-m", "modelforge_node_agent.main"]