# syntax=docker/dockerfile:1.7
#
# A ComfyUI image with the custom-node dependency closure, ComfyUI itself, and
# its frontend already in place.
#
# THE PROBLEM. `core.toml` declares `image = "runpod/comfyui:latest"`, which
# ships almost none of the 19 custom-node packs the operator's manifests
# declare. Every cold start clones all 19 and pip-installs their requirements
# from scratch. Three of those requirements have no wheel on PyPI at all and
# compile against torch (`groundingdino-py`, Impact-Pack's
# `git+https://github.com/facebookresearch/sam2`, and `fairscale`), and the rest
# is roughly a gigabyte of wheels -- onnxruntime-gpu alone is 202 MB. Measured
# live on 2026-08-27/28, the phase before Ollama exceeded 45 minutes on its own.
#
# WHAT IS BAKED, AND WHERE IT HAS TO GO
#
#   pip packages      -> /opt/curu-deps  (a venv, reached via PYTHONPATH)
#   ComfyUI + its own -> /opt/comfyui-baked  (the base image's staging copy)
#   requirements
#   apt packages      -> the final image's own root filesystem
#
# NOT the node repositories. /workspace is a mounted RunPod network volume and
# `CUSTOM_NODES_DIR` is under it, so anything baked at that path is masked the
# moment the pod starts. The same is true of /workspace/runpod-slim/ComfyUI,
# which is why ComfyUI is updated at its STAGING path instead -- see bake.sh.
#
# Built for linux/amd64 only, by .github/workflows/comfy-image.yml. Do not try
# to build it on an arm64 machine: the base image has no arm64 variant and an
# emulated build compiles CUDA extensions under qemu for hours.


# ── builder ───────────────────────────────────────────────────────────────────
#
# Everything transient happens here: 19 git clones, pip's build trees, apt
# lists. None of it can reach the final image, because the final image copies
# two named directories and nothing else. That is a structural guarantee rather
# than a cleanup line that someone later deletes without noticing.
#
# The builder shares the runtime's base on purpose. groundingdino-py, sam2 and
# fairscale compile against the Python ABI and the CUDA runtime; built on a
# different base they would import-fail on a live pod, which surfaces at
# generation time rather than build time and is far worse than a fat image.
FROM runpod/comfyui:latest AS builder

SHELL ["/bin/bash", "-euo", "pipefail", "-c"]

ENV DEBIAN_FRONTEND=noninteractive \
    PIP_ROOT_USER_ACTION=ignore \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# start.sh exports this at runtime but the image does not declare it, so a
# `docker build` would install unconstrained -- free to resolve a torch that is
# not the +cu128 build every compiled extension here links against. Declared so
# the bake happens under exactly the constraints a pod's own pip install does.
ENV PIP_CONSTRAINT=/opt/comfyui-runtime-constraints.txt

# sam2's setup.py builds a CUDA extension by default and already tolerates its
# own build failing (SAM2_BUILD_ALLOW_ERRORS defaults to 1), so on a pod today
# it is best-effort and silently skipped. Turning it off makes that outcome
# deterministic, and skips the nvcc compile of sam2/csrc/connected_components.cu.
#
# BE CLEAR ABOUT WHAT THIS DOES NOT SAVE. It is the small half. pip installs
# `build-system.requires` -- sam2's pyproject names `torch>=2.5.1` -- into the
# isolated build environment BEFORE invoking the backend, so setup.py has not
# run and this variable has not been read by the time ~1.6 GB of torch and
# NVIDIA runtime wheels have already been fetched. Only --no-build-isolation
# (see bake.sh) removes that.
ENV SAM2_BUILD_CUDA=0

# Strict by default: a pack whose pip install fails stops the build. A pod
# tolerates that failure (it marks one node FAILED and carries on), so an
# unbakeable pack is not a reason to ship no image -- but a partial success that
# reports success is how a cache silently stops working. Build with
# `--build-arg BAKE_STRICT=0` once you know which pack is failing and why.
ARG BAKE_STRICT=1

# Both default to the concrete versions recorded in nodes.plan, which
# `curu-infra nodes bake-plan` resolved and `bake_plan_hash` covers. Overriding
# either here pins the image to something the plan does not describe, so the
# plan hash in the tag would no longer identify the contents -- use the plan.
ARG COMFYUI_REF=
ARG COMFYUI_FRONTEND_VERSION=

COPY docker/nodes.plan /opt/curu-bake/nodes.plan
COPY docker/bake.sh /opt/curu-bake/bake.sh

# The pip cache is a BuildKit cache mount, so the ~2 GB of downloaded wheels
# lives outside the layer and outside the final image. It also never reaches the
# runtime stage, which copies only /opt/curu-deps and /opt/comfyui-baked.
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
    BAKE_STRICT="$BAKE_STRICT" \
    COMFYUI_REF="$COMFYUI_REF" \
    FRONTEND_VERSION="$COMFYUI_FRONTEND_VERSION" \
    bash /opt/curu-bake/bake.sh deps


# ── runtime ───────────────────────────────────────────────────────────────────
FROM runpod/comfyui:latest AS runtime

SHELL ["/bin/bash", "-euo", "pipefail", "-c"]

ENV DEBIAN_FRONTEND=noninteractive \
    PIP_ROOT_USER_ACTION=ignore \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# The manifests' own `setup_packages` -- rsync, screen, zstd, lshw. Runtime
# tools that curu-infra shells out to over SSH, so they belong here and not only
# in the builder. Its own apt lists are removed in the same layer.
COPY docker/nodes.plan /opt/curu-bake/nodes.plan
COPY docker/bake.sh /opt/curu-bake/bake.sh
RUN bash /opt/curu-bake/bake.sh apt

# Kept in the image rather than piped in from CI: the same script answers "is
# this pod's image intact?" over SSH, which is where the question is actually
# asked. A few KB.
COPY docker/smoke.py /opt/curu-bake/smoke.py

# The two artefacts, and nothing else the build touched.
COPY --from=builder /opt/curu-deps /opt/curu-deps
COPY --from=builder /opt/comfyui-baked /opt/comfyui-baked

# HOW THE BAKED PACKAGES ARE REACHED. ComfyUI runs from a venv on the mounted
# volume (`/workspace/runpod-slim/ComfyUI/.venv-cu128`, created by start.sh with
# `--system-site-packages`), and curu-infra's own `pip install` arrives over SSH
# in a shell whose PATH start.sh exported before that venv was activated. A
# prefix on PYTHONPATH is visible to both, and start.sh already forwards
# PYTHONPATH and PATH into the SSH environment explicitly -- it greps for both
# by name when it writes /etc/rp_environment. So this is the mechanism the base
# image supports, not a trick played on it.
#
# PYTHONPATH is ahead of site-packages in sys.path, so a pack that needs a newer
# library than the base ships gets it. That is the same outcome a pod reaches
# today by pip-upgrading the system copy in place.
ENV PYTHONPATH=/opt/curu-deps/lib/python3.12/site-packages
ENV PATH=/opt/curu-deps/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# Both also set at runtime by start.sh; declared here so a `docker run` or a
# `docker exec` behaves the same as a pod rather than subtly differently.
ENV PIP_CONSTRAINT=/opt/comfyui-runtime-constraints.txt
ENV SAM2_BUILD_CUDA=0

LABEL org.opencontainers.image.title="curu-comfy" \
      org.opencontainers.image.description="runpod/comfyui with curu-infra's custom-node pip dependencies, ComfyUI and frontend pre-installed" \
      org.opencontainers.image.source="https://github.com/darth-veitcher/curu-infra" \
      org.opencontainers.image.base.name="docker.io/runpod/comfyui:latest"
