이 글은 물리적 GPU 없이 ComfyUI 워크플로우를 API로 제공하고자 하는 숙련된 사용자를 위한 가이드입니다.
RunPod Serverless와 Storage를 활용하여, 모델 및 커스텀 노드를 매번 다운로드할 필요 없이 효율적으로 ComfyUI API 서버를 구축하는 방법을 상세히 다룹니다.
배경: 워크플로우 개발 및 API 전환
RunPod Pods에서 ComfyUI를 이용해 이미지 생성 워크플로우를 개발했다면, 아마도 RunPod Storage를 활용하여 모델, 커스텀 노드, 작업 결과물을 영구적으로 보관하고 있을 겁니다.
이는 Pods를 종료해도 작업 환경을 유지할 수 있게 해주어 매우 효율적입니다.
이제 개발이 완료된 이 워크플로우를 다른 서비스에서 호출할 수 있는 API 형태로 제공해야 합니다.
이를 위해 RunPod의 Serverless Endpoint를 사용하며, 사용자의 API 요청이 들어올 때마다 GPU 컨테이너를 스케일링하여 이미지를 생성하고 결과를 반환하는 방식을 취합니다.
문제는 Serverless 컨테이너가 시작될 때마다 필요한 모델과 커스텀 노드를 다시 다운로드해야 한다는 점입니다.
이는 불필요한 시간 소모와 비용을 발생시킵니다.
이 문제를 해결하기 위해, 기존 Pods에서 사용하던 Storage를 Serverless 컨테이너에 마운트하여 사전 다운로드된 자원을 활용할 수 있습니다.
RunPod Serverless용 ComfyUI Docker 이미지 커스텀 가이드
RunPod Serverless에 최적화된 ComfyUI API를 배포하려면, 공식 runpod-workers/worker-comfyui 레포지토리를 기반으로 Docker 이미지를 커스텀하는 과정이 필요합니다. 이 과정은 API 로직을 통합하고, 필요한 종속성을 미리 설치하여 배포 시간을 최소화하는 데 중점을 둡니다.
https://github.com/runpod-workers/worker-comfyui
GitHub - runpod-workers/worker-comfyui: ComfyUI as a serverless API on RunPod
ComfyUI as a serverless API on RunPod. Contribute to runpod-workers/worker-comfyui development by creating an account on GitHub.
github.com
위 레포지토리를 clone 합니다.
커스텀
기존 Dockerfile에 API 로직과 필요한 라이브러리 설치 단계를 추가해야 합니다.
1. requirements-custom-nodes.txt 추가
프로젝트 루트 위치에 워크플로우 로직에 필요한 Python 라이브러리(예: requests, runpod)를 정의하는 의존성 관리 파일을 생성합니다.
cutom-nodes 는 이미 사전에 작업 과정에서 Storage 에 다운받아져 있지만,
Serverless 의 오토스케일링에 의해 동적으로 GPU PC 컨테이너가 올라가고 내려갈때마다 새로운 깨끗한 PC환경을 받는 것이나 다름없기 때문에, 의존성 패키지들은 매번 설치를 새로 해두어야 합니다.
아래는 사용하는 워크플로우에 구성된 Custom_node 에 필요한 의존성 패키지들만 모아둔 것이다.
만약 새로운 custom_node 를 워크플로우상에 추가한 경우, 그 custom_node 가 필요로하는 의존성을 이 파일에 새롭게 추가하여 docker 이미지를 새로 배포하면 된다.
# Custom nodes dependencies
# Core dependencies
# opencv-python
gguf
numba
piexif
# Machine learning and scientific computing
# scikit-image
# accelerate
# Utilities
# simpleeval
# Hugging Face compatibility fix
# huggingface_hub
# Additional packages that might be needed by various custom nodes
# (uncomment as needed)
# transformers
# requests
# websocket-client
# pandas
blend_modes
segment_anything
numpy<2
cython
onnxruntime-gpu==1.18.1
insightface==0.7.3
facexlib
ftfy
timm
python-dotenv==1.0.1
boto3==1.34.32
diffusers==0.35.1
peft==0.17.1
lark==1.2.2
2. Dockerfile 수정
runpod-workers/worker-comfyui 레포지토리의 Dockerfile을 수정한다.
# Build argument for base image selection
ARG BASE_IMAGE=nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04
# Stage 1: Base image with common dependencies
FROM ${BASE_IMAGE} AS base
# Build arguments for this stage (defaults provided by docker-bake.hcl)
ARG COMFYUI_VERSION=0.3.52
ARG CUDA_VERSION_FOR_COMFY
ARG ENABLE_PYTORCH_UPGRADE
ARG PYTORCH_INDEX_URL=
# Prevents prompts from packages asking for user input during installation
ENV DEBIAN_FRONTEND=noninteractive
# Prefer binary wheels over source distributions for faster pip installations
ENV PIP_PREFER_BINARY=1
# Ensures output from python is printed immediately to the terminal without buffering
ENV PYTHONUNBUFFERED=1
# Speed up some cmake builds
ENV CMAKE_BUILD_PARALLEL_LEVEL=8
ENV PIP_NO_CACHE_DIR=1
# Install Python, git and other necessary tools
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential g++ gcc make pkg-config cmake ninja-build \
python3.11 python3.11-venv python3.11-dev \
git wget \
libgl1 libglib2.0-0 libsm6 libxext6 libxrender1 \
ffmpeg \
&& ln -sf /usr/bin/python3.11 /usr/bin/python \
&& ln -sf /usr/bin/pip3 /usr/bin/pip
# Clean up to reduce image size
RUN apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*
# Install uv (latest) using official installer and create isolated venv
RUN wget -qO- https://astral.sh/uv/install.sh | sh \
&& ln -s /root/.local/bin/uv /usr/local/bin/uv \
&& ln -s /root/.local/bin/uvx /usr/local/bin/uvx \
&& uv venv /opt/venv
# Use the virtual environment for all subsequent commands
ENV PATH="/opt/venv/bin:${PATH}"
# Install comfy-cli + dependencies needed by it to install ComfyUI
RUN uv pip install comfy-cli pip setuptools wheel \
&& uv pip install "numpy<2" \
&& rm -rf /root/.cache/uv /root/.cache/pip
# Install ComfyUI
RUN /usr/bin/yes | comfy --workspace /comfyui install --version "${COMFYUI_VERSION}" --cuda-version "12.4" --nvidia
# Upgrade PyTorch if needed (for newer CUDA versions)
RUN uv pip install torch==2.4.0 torchvision==0.19.0 torchaudio==2.4.0 --index-url https://download.pytorch.org/whl/cu124
# Change working directory to ComfyUI
WORKDIR /comfyui
# Support for the network volume
ADD src/extra_model_paths.yaml ./
# Go back to the root
WORKDIR /
# Install Python runtime dependencies for the handler
RUN uv pip install runpod requests websocket-client \
&& rm -rf /root/.cache/uv /root/.cache/pip
# Copy and install common dependencies for custom nodes
COPY requirements-custom-nodes.txt /tmp/requirements-custom-nodes.txt
RUN uv pip install -r /tmp/requirements-custom-nodes.txt \
&& rm -rf /root/.cache/uv /root/.cache/pip \
&& find /opt/venv -type d -name '__pycache__' -prune -exec rm -rf {} +
# Add application code and scripts
ADD src/start.sh handler.py test_input.json ./
RUN chmod +x /start.sh
# Prevent pip from asking for confirmation during uninstall steps in custom nodes
ENV PIP_NO_INPUT=1
# Copy helper script to switch Manager network mode at container start
COPY scripts/comfy-manager-set-mode.sh /usr/local/bin/comfy-manager-set-mode
RUN chmod +x /usr/local/bin/comfy-manager-set-mode
# Set the default command to run when starting the container
CMD ["/start.sh"]
# Stage 3: Final image
FROM base AS final
Dockerfile 의 변경점 분석
기존 comfy-worker 에서 제공하는 Dockerfile 과 무엇이 달라졌는가?
| 변경된 부분 | Custom Dockerfile | worker-comfyui Dockerfile |
| 베이스 이미지 | nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04 | nvidia/cuda:12.6.3-cudnn-runtime-ubuntu24.04 |
| Python 버전 | python3.11, python3.11-venv | python3.12, python3.12-venv |
| uv 캐시 | rm -rf /root/.cache/uv /root/.cache/pip (여러 번 등장) | uv pip install 이후 캐시 삭제 명령 없음 |
| PyTorch 업그레이드 | uv pip install torch==2.4.0 ... (하드코딩) | if [ "$ENABLE_PYTORCH_UPGRADE" = "true" ]; then ... (변수 기반) |
| 커스텀 노드 | requirements-custom-nodes.txt 파일을 복사하고 설치 | comfy-node-install.sh 스크립트를 복사 |
| 모델 다운로드 | 없음 | **downloader**라는 별도 빌드 스테이지에서 모델 다운로드 |
Dockerfile 의 변경된 이유 분석
1. 베이스 이미지 및 Python 버전
- 변경 이유: Runpod 에서 Pods 를 올려서 워크플로우를 테스트 할 때, 사용한 Runpod Pytorch 이미지 버전과 맞추기 위함
- Runpod Pytorch 2.4.0 : runpod/pytorch:2.4.0-py3.11-cuda12.4.1-devel-ubuntu22.0.4
2. uv 캐시 삭제
- 변경 이유: Docker 이미지 크기를 최소화하기 위함입니다. uv pip install 명령을 실행할 때마다 생성되는 캐시 파일을 수동으로 삭제하여, 최종 이미지에 불필요한 용량이 포함되지 않도록 합니다. Serverless 환경에서는 이미지 크기가 로딩 시간에 영향을 줄 수 있으므로 중요한 최적화 단계입니다.
3. PyTorch 업그레이드
- 변경 이유: 원본 Dockerfile은 ENABLE_PYTORCH_UPGRADE라는 빌드 인자를 통해 동적으로 PyTorch 업그레이드 여부를 결정합니다. 반면 커스텀 Dockerfile은 특정 버전(2.4.0)을 명시적으로 설치합니다. 이는 개발 환경과 동일한 PyTorch 버전을 강제하여 **재현성(Reproducibility)**을 확보하고, 워크플로우 실행 중 발생할 수 있는 호환성 문제를 방지하기 위함입니다.
4. 커스텀 노드 설치 방식
- 변경 이유: 원본 Dockerfile은 comfy-node-install.sh 스크립트를 사용하여 컨테이너 실행 시점에 노드를 설치하는 유연성을 제공합니다. 이는 모든 노드를 이미지에 포함하는 대신, 사용자가 원하는 노드만 동적으로 설치할 수 있게 합니다.
- 하지만 커스텀 Dockerfile은 requirements-custom-nodes.txt 파일을 빌드 시점에 복사하여 uv pip install로 필요한 패키지를 미리 설치합니다. 이렇게 하면 컨테이너가 시작될 때마다 설치 과정을 거칠 필요 없이 바로 사용 가능하므로 콜드 스타트(Cold Start) 시간을 단축할 수 있습니다.
- 스토리지를 연결하여 사용하기 때문에, 이미 커스텀 노드관련된 파일들은 다운받아져 있는 상태이고, 필요한 의존성만 설치하면 되어 이렇게 구성했습니다.
5. 모델 다운로드
- 변경 이유: 가장 큰 차이점입니다. 원본 Dockerfile은 downloader라는 별도 빌드 스테이지를 통해 미리 정의된 모델들을 이미지에 포함시킵니다. 이는 컨테이너가 시작될 때 모델을 다운로드하는 시간을 없애 주어 콜드 스타트 시간을 획기적으로 줄이지만, 이미지 용량이 매우 커진다는 단점이 있습니다.
- 커스텀 Dockerfile에는 이 downloader 스테이지가 없습니다. 대신, RunPod Storage를 마운트하여 모델을 로드하는 방식을 사용합니다. 이는 다음 두 가지 장점이 있습니다.
- 이미지 크기 최적화: 모델 파일이 이미지에 포함되지 않으므로 Docker 이미지 크기를 최소화할 수 있습니다.
- 유연성 및 효율성: 모델을 이미지에 굽는 대신, Pods 개발 환경에서 사용하던 동일한 Storage를 Serverless 컨테이너에 마운트하여 사용합니다. 이는 모델 업데이트가 있을 때마다 이미지를 새로 빌드하고 푸시할 필요 없이, Storage의 모델만 교체하면 되므로 매우 효율적입니다.
이처럼 커스텀 Dockerfile은 RunPod Storage를 활용하는 전략을 중심으로, 이미지 크기를 최적화하고 콜드 스타트 시간을 줄이는 데 초점을 맞추고 있습니다.
3. src/extra_model_paths.yaml
스토리지에서 미리 다운받아놓은 모델들의 경로를 Serverless Gpu 컨테이너에 매핑하기 위한 yaml 파일 입니다.
사용하는 모델들 경로를 작성하면 됩니다.
runpod_worker_comfy:
base_path: /runpod-volume/ComfyUI/models
checkpoints: checkpoints/
clip: clip/
clip_vision: clip_vision/
configs: configs/
controlnet: controlnet/
diffusers: diffusers/
diffusion_models: diffusion_models/
dreamo: dreamo/
embeddings: embeddings/
facexlib: facexlib/
gligen: gligen/
hypernetworks: hypernetworks/
loras: loras/
photomaker: photomaker/
pulid: pulid/
rembg: rembg/
style_models: style_models/
text_encoders: text_encoders/
unet: unet/
upscale_models: upscale_models/
vae: vae/
vae_approx: vae_approx/
wget-log: wget-log/
insightface: insightface/
여기서 insightface 의 경우, runpod-worker 에서 모델로 인식하지 못하는 이슈가 있습니다.
이로 인해 실제 api 로 pulid 기반 워크플로우를 돌려봤을때, 페이스 디텍트가 제대로 되지 않아서 (insightface 의 역할 : Face detection) 인풋으로 사용한 얼굴의 특징이 반영되지 않은 페이스 스왑 결과물을 받는 이슈가 있었습니다.
이를 해결하기 위해 아래 Start.sh 에서 추가적인 설명을 합니다.
insightface 를 사용하지 않더라도, 아래 start.sh 수정이 필요합니다.
4. Start.sh 수정
src/start.sh 을 수정합니다.
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# Copy all custom_nodes from network volume to local directory
if [ -d "/runpod-volume/ComfyUI/custom_nodes" ]; then
echo "ls -la /runpod-volume/ComfyUI/custom_nodes"
ls -la /runpod-volume/ComfyUI/custom_nodes/
# Remove any existing symlinks or directories that might conflict
rm -rf /comfyui/custom_nodes/custom_nodes
# Copy all custom nodes
cp -r /runpod-volume/ComfyUI/custom_nodes/* /comfyui/custom_nodes/ 2>/dev/null || echo "worker-comfyui: No files to copy or copy failed"
echo "worker-comfyui: Custom nodes copied successfully"
echo "ls -la /comfyui/custom_nodes/"
ls -la /comfyui/custom_nodes/
else
echo "worker-comfyui: No custom_nodes directory found in Network Volume"
fi
# Check if /runpod-volume/ComfyUI/models/insightface is mounted
if [ -d "/runpod-volume/ComfyUI/models/insightface" ]; then
echo "/runpod-volume/ComfyUI/models/insightface directory exists."
# Create a soft link to /comfyui/models/insightface if it doesn't already exist 목적지에 없으면,
if [ ! -L "/comfyui/models/insightface" ]; then
ln -s /runpod-volume/ComfyUI/models/insightface /comfyui/models/insightface
echo "Created a soft link to /comfyui/models/insightface."
else
echo "Soft link already exists."
fi
else
echo "/runpod-volume/ComfyUI/models/insightface directory does not exist."
fi
# Link output directory
echo "worker-comfyui: Linking output directory..."
ln -sf /runpod-volume/ComfyUI/output /comfyui/output
echo "worker-comfyui: Verifying setup..."
echo "/comfyui/custom_nodes/."
ls -la /comfyui/custom_nodes/ || echo "ERROR: /comfyui/custom_nodes directory check failed"
echo "/comfyui/output"
ls -la /comfyui/output || echo "ERROR: /comfyui/output link failed"
# ─────────────────────────────────────────────────────────────
# Use libtcmalloc for better memory management
TCMALLOC="$(ldconfig -p | grep -Po "libtcmalloc.so.\d" | head -n 1)"
export LD_PRELOAD="${TCMALLOC}"
# Ensure ComfyUI-Manager runs in offline network mode inside the container
comfy-manager-set-mode offline || echo "worker-comfyui - Could not set ComfyUI-Manager network_mode" >&2
echo "worker-comfyui: Starting ComfyUI"
# Allow operators to tweak verbosity; default is DEBUG.
: "${COMFY_LOG_LEVEL:=DEBUG}"
# Serve the API and don't shutdown the container
if [ "$SERVE_API_LOCALLY" == "true" ]; then
python -u /comfyui/main.py --disable-auto-launch --disable-metadata --listen --verbose "${COMFY_LOG_LEVEL}" --log-stdout &
echo "worker-comfyui: Starting RunPod Handler"
python -u /handler.py --rp_serve_api --rp_api_host=0.0.0.0
else
python -u /comfyui/main.py --disable-auto-launch --disable-metadata --verbose "${COMFY_LOG_LEVEL}" --log-stdout &
echo "worker-comfyui: Starting RunPod Handler"
python -u /handler.py
fi
start.sh 의 변경점 분석
두 스크립트의 주요 차이점은 스크립트 상단에 추가된 Storage 관련 파일 및 디렉터리 처리 로직입니다.
| 변경된 부분 | Custom start.sh | worker-comfyui start.sh (원본) |
| 커스텀 노드 복사 | /runpod-volume/ComfyUI/custom_nodes에서 /comfyui/custom_nodes로 파일을 복사(cp) | 관련 스크립트 없음 |
| insightface 모델 링크 | /runpod-volume/ComfyUI/models/insightface에 대한 심볼릭 링크(ln -s)를 /comfyui/models/insightface에 생성 | 관련 스크립트 없음 |
| output 디렉터리 링크 | /runpod-volume/ComfyUI/output에 대한 심볼릭 링크(ln -sf)를 /comfyui/output에 생성 | 관련 스크립트 없음 |
| 핵심 실행 로직 | 기존의 python -u /comfyui/main.py와 python -u /handler.py 실행 로직은 동일 | 기존의 python -u /comfyui/main.py와 python -u /handler.py 실행 로직은 동일 |
원본 start.sh와 커스텀 start.sh의 가장 큰 차이점은 RunPod Storage 마운트와 관련된 추가적인 스크립트입니다.
커스텀 스크립트는 컨테이너가 시작될 때 Storage에 있는 custom_nodes 자원을 컨테이너 내부로 가져오는 역할을 합니다.
+ # Check if /runpod-volume/ComfyUI/models/insightface is mounted
+ insightface 의 경우에는 모델로 인식하지 못하여 (extra_model_paths.yaml 파일에 매핑이 동작하지 않음)
+ 추가로 복사를 해주어야 합니다.
start.sh 의 변경된 이유 분석
이러한 변경은 **"컨테이너가 시작될 때마다 모델과 커스텀 노드를 다운로드하지 않고, 이미 존재하는 RunPod Storage를 활용한다"**는 핵심 전략을 구현하기 위함입니다.
1. 커스텀 노드 복사
- 커스텀 노드의 경로를 읽지 못하는 이슈가 있어서 통째로 컨테이너에 복사하는 것으로 수정했다.
2. insightface 모델 심볼릭 링크
- 가장 이슈가 많았던 부분...
- pulid 를 사용하면 insightface 를 수동 설치하고, 압축을 해제하여 정해진 폴더에 세팅하는 단계가 있다.
- https://github.com/balazik/ComfyUI-PuLID-Flux?tab=readme-ov-file#installation
- Finally you need InsightFace with AntelopeV2, the unzipped models should be placed in ComfyUI/models/insightface/models/antelopev2.
- 위의 작업이 컨테이너 상에서 제대로 이루어지지 않아서 페이스 디텍트가 정상적으로 되지않는 이슈가 있어서, 통째로 복사하는 로직을 추가 했습니다.
- 관련된 동일한 이슈에 대한 comfy-worker 레포의 Issue 링크입니다.
- 동일한 이슈를 해결하기 위해 start.sh 에 복사하는 방법으로 처리한 블로그 글 입니다.
Configuring insightface model paths with yaml file · Issue #5280 · comfyanonymous/ComfyUI
Expected Behavior Leaving ComfyUI\models\insightface empty, whereas having an external path to the actual localization of the models, via .yaml file: comfyui: base_path: ExternalPath ... pulid: mod...
github.com
Deploying a ComfyUI Workflow on a Serverless Runpod Worker
An uphill battle with python, comfyui and docker.
www.mikedegeofroy.com
'AI' 카테고리의 다른 글
| ComfyUI - Load from S3 (S3 에 업로드된 이미지를 인풋으로 사용하기) (0) | 2025.09.10 |
|---|---|
| [AI] Claude code 에게 페르소나 부여하기 (1) | 2025.09.01 |
| ComfyUI 에서 생성한 output 결과물 S3에 업로드하기 (0) | 2025.08.19 |
| MacBook에서 GPU 없이 ComfyUI Docker 로컬 실행하기 (0) | 2025.08.12 |
| [Error] ComfyUI 발생 에러 PulidFluxInsightFaceLoader (1) | 2025.08.11 |