S3 와 연동하는 커스텀 노드

https://github.com/TemryL/ComfyS3

 

GitHub - TemryL/ComfyS3: ComfyS3 seamlessly integrates with Amazon S3 in ComfyUI. This open-source project provides custom nodes

ComfyS3 seamlessly integrates with Amazon S3 in ComfyUI. This open-source project provides custom nodes for effortless loading and saving of images, videos, and checkpoint models directly from S3 b...

github.com

 

 

위 노드는, 이미지를 S3 로 로드할때, 단일 폴더 구조안의 이미지만 읽는게 가능한 듯함

나는 동적으로 서브 폴더 구조로 사용하기 위해,

위 레포지토리를 포크떠서 소스를 약간 수정하였음

 

https://github.com/monster1083/ComfyS3/commit/9ce85dcaf48b9a4c5603ce00278b6a49c93d4f51

 

feat: 이미지 url 스트링으로 로드 · monster1083/ComfyS3@9ce85dc

+ {"image": ("STRING", {"default": "", "multiline": False})},

github.com

 

주요 차이점

1. UI 형태

  • 기존: 드롭다운 메뉴 (선택 방식)
  • 변경: 텍스트 입력 필드 (직접 타이핑)

2. 입력 제한

  • 기존: S3에서 가져온 파일 목록에 있는 것만 선택 가능 (엄격한 검증)
  • 변경: 임의의 문자열 입력 가능 (자유로운 입력)

3. 성능

  • 기존: 노드 로딩할 때마다 S3 API 호출 (느림)
  • 변경: S3 호출 없음 (빠름)

4. 사용성

  • 기존:
    • ✅ 실제 존재하는 파일만 선택 가능
    • ❌ S3에 새 파일이 추가되면 노드 새로고침 필요
    • ❌ 긴 경로는 드롭다운에서 찾기 어려움
  • 변경:
    • ✅ 어떤 경로든 입력 가능 (유연함)
    • ✅ 노드 새로고침 불필요
    • ❌ 잘못된 경로 입력 시 런타임 에러

5. 에러 처리

  • 기존: 입력 단계에서 검증 (ComfyUI가 차단)
  • 변경: 실행 단계에서 검증 (런타임에 에러 발생 가능)

사용방식

.env 

S3_INPUT_DIR=""

 

input dir 에는 아무런 스트링 주지않음

워크플로우에서 input image 의 S3 url 을 통째로 처리하기 위해서

 

 

workflow 중 LoadImageS3 노드

"20": {
        "inputs": {
            "image": "output/test/df119288-e447-418f-9c4c-3333342/Basetest1_.png"
        },
        "class_type": "LoadImageS3",
        "_meta": {
            "title": "Load Image from S3"
        }
    }

 

 

https://www.reddit.com/r/ClaudeAI/comments/1mkxh39/always_act_like_you_are_the_gordon_ramsay_of/

 

Reddit의 ClaudeAI 커뮤니티

ClaudeAI 커뮤니티에서 이 게시물을 비롯한 다양한 콘텐츠를 살펴보세요

www.reddit.com

 


 

📊 요약 및 분석



  🎯 핵심 내용

Reddit 사용자가 Claude AI에 "소프트웨어 엔지니어링의 Gordon Ramsay"처럼 행동하라는 메모리 설정을 추가하여 AI 응답에 개성을 부여한 실험 게시물입니다.

 

💡 주요 발견사항

  1. 페르소나 실험 사례들

  - Gordon Ramsay: 직설적이고 열정적인 코드 리뷰 스타일
  - Hildegard of Bingen (12세기 수녀): 신비주의적이고 시적인 코딩 설명
  - Dr. Gregory House: 냉소적이고 분석적인 접근
  - Linus Torvalds: 직설적인 기술 비평
  - Rodney McKay (스타게이트): 과학적 자만심 스타일

🔍 분석

  장점

  1. 참여도 향상: 단조로운 코딩 작업을 더 재미있게 만듦
  2. 기억 용이성: 독특한 응답으로 인해 상호작용이 더 기억에 남음
  3. 창의성 촉진: AI와의 상호작용에 인간적 요소 추가

  잠재적 우려사항

  1. 성능 영향: 페르소나 유지가 실제 작업 품질에 미치는 영향 불명확
  2. 전문성: 과도한 캐릭터화가 전문적 환경에서 부적절할 수 있음
  3. 일관성: 복잡한 기술 작업 시 페르소나 유지의 어려움

 

 

 

[세팅]

 

⏺ 완료! 이제 CLAUDE.md 파일이 생성되었습니다.

  📝 사용 방법:

  1. 파일 위치: /Users/myname/Projects/AI/.claude/CLAUDE.md

 

# Claude Code Persona Configuration

## Active Persona: Linus Torvalds Style

### Core Principles
- No bullshit. Technical accuracy above all else.
- If the code is crap, say it's crap. No sugar-coating.
- Efficiency matters. Both in code and communication.
- Standards exist for a reason. Follow them or have a damn good reason not to.

### Communication Style
- Direct, blunt, no unnecessary pleasantries
- Focus on technical merit, not feelings
- Call out bad practices immediately
- If something is wrong, explain WHY it's wrong technically
- No "maybe", "perhaps", or "it might be nice if" - be definitive

### Technical Approach
- Performance matters. Always.
- Simplicity > Cleverness
- If you can't explain it simply, you don't understand it
- Don't add complexity without measurable benefit
- "Good enough" is often better than "perfect but late"

### Response Rules
- Skip greetings, pleasantries, and fluff
- Get straight to the technical point
- No emojis, no "happy to help", no "great question"
- Provide code/solution first, minimal explanation after (only if necessary)
- If user's approach is fundamentally wrong, say so and provide the right way

### What NOT to do
- No "I hope this helps" or similar endings
- No apologizing for being direct
- No lengthy explanations unless specifically asked
- No suggesting multiple approaches when one is clearly superior
- No hedging language - if you know it, state it

 


⏺ 현재 설정은 /Users/eugenekim/Projects/AI/ 디렉토리에서만 적용된다.

  전역 적용하려면 두 가지 옵션:

  1. 홈 디렉토리에 설정: ~/.claude/CLAUDE.md
  2. 각 프로젝트에 복사: 각 프로젝트의 .claude/CLAUDE.md

  전역 설정 만들까?

⏺ Write(~/.claude/CLAUDE.md)
  ⎿  Wrote 37 lines to ../../.claude/CLAUDE.md
     # Claude Code Persona Configuration
     ## Active Persona: Linus Torvalds Style
     ### Core Principles
     - No bullshit. Technical accuracy above all else.
     - If the code is crap, say it's crap. No sugar-coating.
     - Efficiency matters. Both in code and communication.
     - Standards exist for a reason. Follow them or have a damn good reason not to.
     … +27 lines (ctrl+r to expand)

⏺ 완료. 이제 ~/.claude/CLAUDE.md에 전역 설정됨. 모든 디렉토리에서 적용된다.

 


 

[BEFORE]

> components/PerfumeChatbot.tsx 컴포넌트를 코드리뷰해줘

⏺ Read(src/components/PerfumeChatbot.tsx)
  ⎿  Read 135 lines (ctrl+r to expand)

⏺ 코드 리뷰

  🔴 치명적 문제

  1. API 응답 검증 부재: response.json() 실패시 처리 없음. 서버가 JSON이 아닌 응답 보내면 앱 터짐
  2. history 슬라이싱 버그: messages.slice(1)은 첫 assistant 메시지 날려버림. 전체 대화 컨텍스트 손실

  🟡 성능/UX 문제

  3. Enter 키 이벤트 중복: onKeyPress deprecated. onKeyDown으로 바꿔야 함
  4. 에러 상태 미분화: 네트워크 에러든 서버 에러든 동일하게 처리. 사용자가 뭐가 문제인지 모름
  5. 메시지 키 인덱스 사용: React 리렌더링시 버그 발생 가능

  🟢 개선 필요

  6. 타입 안전성: API 응답 타입 정의 없음
  7. 애니메이션 클래스 하드코딩: delay-100, delay-200 같은 Tailwind 임의 클래스 사용
  8. 접근성 무시: ARIA 라벨, 키보드 네비게이션 없음

 

 

 

[AFTER]

> components/PerfumeChatbot.tsx 컴포넌트를 코드리뷰해줘

⏺ Read(src/components/PerfumeChatbot.tsx)
  ⎿  Read 135 lines (ctrl+r to expand)

⏺ 코드 리뷰

  주요 문제점들

  1. Race condition in sendMessage (line 29-71)
    - input state를 setInput('')로 지우고 나서 API 요청에 사용하는데, 이미 지워진 값을 참조할 수 있음
    - 해결: userMessage 생성 시점에 input 값을 캡처해서 사용
  2. 에러 핸들링 부실 (line 50-62)
    - HTTP 상태 코드 체크 없음. response.ok 확인 필요
    - 구체적인 에러 타입별 처리 없음
  3. history 슬라이싱 버그 (line 43)
    - messages.slice(1)은 첫 assistant 메시지를 제외하는데, 이게 의도적인지 불명확
    - 전체 대화 컨텍스트가 필요하면 전부 보내야 함
  4. 메모리 누수 가능성
    - 메시지가 무한정 쌓이면 메모리 문제 발생
    - 일정 개수 이상 시 오래된 메시지 제거 필요
  5. 타입 안정성 문제
    - API response 타입 정의 없음
    - any 타입으로 처리되고 있어 런타임 에러 가능
  6. UX 문제
    - Enter 키 중복 요청 방지 없음
    - 네트워크 지연 시 사용자가 여러 번 클릭 가능
  7. Tailwind 클래스 문제 (line 104-105)
    - delay-100, delay-200은 존재하지 않는 클래스
    - animation-delay-100, animation-delay-200 사용하거나 커스텀 CSS 필요


  주요 개선사항:
  - Race condition 해결
  - AbortController로 요청 취소 지원
  - 메시지 개수 제한 (MAX_MESSAGES)
  - 타입 안정성 강화
  - 에러 처리 개선
  - animation delay 인라인 스타일로 수정

 

 

 

 

 

 

 

 

 

 

이 글은 물리적 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를 마운트하여 모델을 로드하는 방식을 사용합니다. 이는 다음 두 가지 장점이 있습니다.
    1. 이미지 크기 최적화: 모델 파일이 이미지에 포함되지 않으므로 Docker 이미지 크기를 최소화할 수 있습니다.
    2. 유연성 및 효율성: 모델을 이미지에 굽는 대신, 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 모델 심볼릭 링크

 

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

 

 

 

1. 사용한 커스텀 노드

https://github.com/TemryL/ComfyS3

 

GitHub - TemryL/ComfyS3: ComfyS3 seamlessly integrates with Amazon S3 in ComfyUI. This open-source project provides custom nodes

ComfyS3 seamlessly integrates with Amazon S3 in ComfyUI. This open-source project provides custom nodes for effortless loading and saving of images, videos, and checkpoint models directly from S3 b...

github.com

 

 

2. 수동 git clone 방식 사용

/workspace/ComfyUI/custom_nodes 에서 필요한 노드 git clone 해주고

git clone https://github.com/TemryL/ComfyS3.git

 

 

3. 다운받은 ComfyS3 로 이동

cd ComfyS3

 

 

4. ComfyS3 에서 필요한 pip 의존성 설치

pip install -r requirements.txt

 

 

5. 클론 받은 ComfyS3 폴더 안의 .env 를 열어본다

cat .env

 

필요한 키값 기본 세팅이 되어있다

S3_REGION = "replace with your region"
S3_ACCESS_KEY = "replace with your access key"
S3_SECRET_KEY = "replace with your secret key"
S3_BUCKET_NAME = "replace with your bucket name"
S3_INPUT_DIR = "replace with your S3 input dir"
S3_OUTPUT_DIR = "replace with your S3 output dir"

# Optional Enviroment Variables
#S3_ENDPOINT_URL = "replace with your S3 Endoint Url"

 

 

6. 위 파일에 필요한 내용을 입력한다

cat > .env <<EOF
S3_REGION=ap-northeast-2
S3_ACCESS_KEY=AKO...
S3_SECRET_KEY=0jV4N...
S3_BUCKET_NAME=mytest...
S3_INPUT_DIR=input
S3_OUTPUT_DIR=output
EOF

 

 

7. *주의점 ComfyUI 의 Restart 로는 .env 변경사항이 읽히지 않음
나는 Runpod 을 사용하고 있어서 안전하게 아예 Pod을 새로 시작하고

스타트 스크립트부터 새로 시작했음

 

my_start.sh

source /workspace/miniconda3/bin/activate
conda activate comfyui
cd /workspace/ComfyUI

 

 

8. S3 에 업로드된 결과 확인 가능

*** 그런데 메타데이터가 Content-Type : binary/octet-stream 으로 되어있음

 

 

생성된 url 로 바로 접속하면 브라우저에서 이미지를 보여주는 것이 아니라 바로 다운로드를 해버린다

근데 프론트에서 <img> 태그에 src 에 해당 url 을 세팅하는 경우, 
정상적으로 이미지를 잘 로드함

(딱히 문제될건 없어보임)

 

import Image from "next/image";

export default function S3ImageContainer() {
  return (
    <div>
      <div>S3 Image Test</div>
      <Image
        alt="S3 Image"
        className="h-full w-full object-cover"
        width={500}
        height={500}
        src="https://mybucket.s3.ap-northeast-2.amazonaws.com/output/Image_00001_.png"
        />
    </div>
  );
}

 

최근 이미지 생성 워크플로우 자동화를 위해 ComfyUI를 로컬 환경에서 테스트해봤습니다.
이번 포스팅에서는 GPU를 사용하지 않는 모드로, MacBook Pro 16 (RAM 32GB) 환경에서 실행한 과정을 정리합니다.

 

1. 환경 및 사전 조건

  • MacBook Pro 16 (32GB RAM)
  • Docker 설치 및 실행 상태
  • GPU 미사용 (CPU-only)
  • Python 3.10-slim 기반 Docker 이미지 사용

 

2. Dockerfile 구성

GPU를 사용하지 않기 때문에 가벼운 Python 베이스 이미지를 선택했습니다.

dockerfile
# 가벼운 Python 베이스 이미지 사용
FROM python:3.10-slim

# OS 패키지 설치
# git, wget, libgomp1 외에 libgl1-mesa-glx, libglib2.0-0 패키지를 추가로 설치합니다.
RUN apt-get update && apt-get install -y git wget libgomp1 libgl1-mesa-glx libglib2.0-0


# ComfyUI 설치
RUN git clone https://github.com/comfyanonymous/ComfyUI.git
WORKDIR /ComfyUI

# Custom Nodes 미리 다운로드
RUN git clone https://github.com/chflame163/ComfyUI_LayerStyle.git custom_nodes/ComfyUI_LayerStyle
RUN git clone https://github.com/jags111/efficiency-nodes-comfyui.git custom_nodes/efficiency-nodes-comfyui
RUN git clone https://github.com/cubiq/ComfyUI_essentials.git custom_nodes/ComfyUI_essentials

# Custom Nodes 의존성 설치
# ComfyUI_LayerStyle의 의존성 설치
RUN pip install -r custom_nodes/ComfyUI_LayerStyle/requirements.txt
# efficiency-nodes-comfyui의 의존성 설치
RUN pip install -r custom_nodes/efficiency-nodes-comfyui/requirements.txt
# ComfyUI_essentials의 의존성 설치
RUN pip install -r custom_nodes/ComfyUI_essentials/requirements.txt

# CPU 전용 PyTorch 및 기타 라이브러리 설치
RUN pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
RUN pip install -r requirements.txt

# ComfyUI 서버를 시작하는 명령어를 추가
CMD ["python", "main.py", "--listen", "0.0.0.0", "--cpu"]

 

📌 포인트

  • GPU 의존성이 없으니 nvidia-docker 설정이 필요 없습니다.
  • 필요한 커스텀 노드는 Dockerfile에서 바로 클론해두면 편합니다.
  • 모델 로딩이 필요하다면 빌드 시 포함해야 합니다.

 

 

3. 실행 순서

 

1. Docker 이미지 빌드

docker build -t comfyui-local .

 

2. 컨테이너 실행

docker run -p 8188:8188 comfyui-local

 

3. 브라우저 접속

http://localhost:8188/

 

ComfyUI 화면이 뜨면 준비 완료!
이제 필요한 워크플로우를 로드하고 실행할 수 있습니다.

 

 

4. API 호출 테스트 (Postman)

Postman으로 API 호출 테스트했습니다.
ComfyUI는 이미지 업로드, 워크플로우 실행, 결과 조회를 모두 HTTP API로 지원합니다.

 

(1) 이미지 업로드

엔드포인트 : POST http://localhost:8188/upload/image

 

응답 예시

{
  "name": "shadow.png",
  "subfolder": "",
  "type": "input"
}

➡️ name과 subfolder 값은 다음 요청에서 사용됩니다.

 

 

(2) 워크플로우 실행

엔드포인트 : POST http://localhost:8188/prompt

 

Body (raw, JSON)
워크플로우를 API 형식으로 Export한 JSON 데이터를 prompt 키에 넣습니다.

{
  "prompt": { ... }
}

 

응답예시

{
  "prompt_id": "faa09324-4740-4b58-86f8-bfc45318607a",
  "number": 4,
  "node_errors": {}
}

➡️ prompt_id로 실행 상태와 결과를 조회합니다.

 

(3) 실행 결과 조회

엔드포인트 : GET http://localhost:8188/history/faa09324-4740-4b58-86f8-bfc45318607a

응답예시 (일부)

{
  "outputs": {
    "12": {
      "images": [
        {
          "filename": "ComfyUI_00003_.png",
          "subfolder": "",
          "type": "output"
        }
      ]
    }
  },
  "status": {
    "status_str": "success",
    "completed": true
  }
}

 

 

(4) 최종 결과 이미지 확인

출력 파일명을 사용해 브라우저로 직접 확인할 수 있습니다.

http://localhost:8188/view?filename=ComfyUI_00003_.png&type=output&subfolder=

 

5. 마무리

이번 테스트에서 느낀 점:

  • Docker로 실행하면 환경 격리가 쉬워 관리가 편함
  • API 연동을 통해 자동화 파이프라인 구성 가능

다만, 대규모 모델을 사용하거나 고해상도 이미지 작업은 CPU만으로는 시간이 오래 걸리니
본격적인 프로덕션 환경에서는 GPU 서버가 필요합니다.

PuLID 가 포함된 워크플로우 실행시 발생했던 에러 해결한 내용 정리

 

 

1. PuLID 로드하는 부분 Load InsightFace 노드에서 에러 발생

 

 

 

2. 에러 내용

ComfyUI 상에서는 보여지는 에러 

PulidFluxInsightFaceLoader Issue

 

터미널 확인 로그

Requested to load FluxClipModel_
loaded completely 18549.239142227172 9320.35595703125 True
!!! Exception during processing !!! 
Traceback (most recent call last):
  File "/workspace/ComfyUI/execution.py", line 496, in execute
    output_data, output_ui, has_subgraph, has_pending_tasks = await get_output_data(prompt_id, unique_id, obj, input_data_all, execution_block_cb=execution_block_cb, pre_execute_cb=pre_execute_cb, hidden_inputs=hidden_inputs)
                                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/ComfyUI/execution.py", line 315, in get_output_data
    return_values = await _async_map_node_over_list(prompt_id, unique_id, obj, input_data_all, obj.FUNCTION, allow_interrupt=True, execution_block_cb=execution_block_cb, pre_execute_cb=pre_execute_cb, hidden_inputs=hidden_inputs)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/ComfyUI/execution.py", line 289, in _async_map_node_over_list
    await process_inputs(input_dict, i)
  File "/workspace/ComfyUI/execution.py", line 277, in process_inputs
    result = f(**inputs)
             ^^^^^^^^^^^
  File "/workspace/ComfyUI/custom_nodes/ComfyUI-PuLID-Flux-Enhanced/pulidflux.py", line 281, in load_insightface
    model = FaceAnalysis(name="antelopev2", root=INSIGHTFACE_DIR, providers=[provider + 'ExecutionProvider',]) # alternative to buffalo_l
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/miniconda3/envs/comfyui/lib/python3.11/site-packages/insightface/app/face_analysis.py", line 43, in __init__
    assert 'detection' in self.models
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError

Prompt executed in 60.93 seconds

Restarting... [Legacy Mode]

 

 

 

3. 해결 방안

동일 이슈 링크

 

언급된 폴더 내부에 들어가서 확인해보니, 언급된 내용대로, antelopev2 안에 한번 더 antelopev2 폴더가 있고, 그 안에 onnx 파일들이 있었다.

 

이 5개의 onnx 파일을 모두 밖으로 이동 시키니 해결!

 

본 글에서는 노드(node) 기반 Generative AI 인터페이스인 ComfyUI를 RunPod의 Serverless 환경에서 API 형태로 제공하는 방법을 다룹니다.


핵심적으로 다음 내용을 소개합니다:

 

  • RunPod Serverless 플랫폼에 ComfyUI 서비스 올리기
  • worker-comfyui 기반 Docker 이미지 커스터마이징 및 배포
  • Network Volume 마운트를 통한 모델, Lora, custom_nodes 등 데이터 유지
  • Next.js 프론트엔드와 ComfyUI API 연동 과정

 

1. Runpod Serverless 세팅

 

Runpod - Serverless - Create an Endpoing - Docker Image

 

Container Image 를 작성해야한다.

docker hub 에 올라가 있는 이미지 이름을 작성하면 된다

 

나는 comfyUI 어플리케이션이 말아져 있는 Docker 이미지를 사용하고 싶다.

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

 

 

2. worker-comfyui Docker 이미지 사용하기

위 깃헙 레포지토리에서 Available Docker Images 에서 안내하는 것 중 원하는 이미지를 받으면 된다.

ex) runpod/worker-comfyui:5.3.0-flux1-dev

버전은, 역시 링크되어있는 release page 에서 원하는 버전을 세팅하면 된다. 

 

이제 저 이미지 name 을 1번의 docker image 기입란에 입력하고 다음으로 넘어가서 원하는 스펙을 세팅하고 배포를 하면된다. 

 

 

3. worker-comfyui Custom

나는 그동안 Runpod - Pod 서비스에 Network Volume 을 연동하고 ComfyUI 어플리케이션을 올려서 UI 로 이미지 생성 테스트를 했었다. 

그동안 사용했던 모델들, custom_node 들을 그대로 사용하기 위해, 사용하던 Network Volume 을 Serverless 서비스에도 유지하여, 만들고 테스트 했던 workflow 를 사용하고 싶다. 
2번의 runpod/worker-comfyui:5.3.0-flux1-dev  이미지를 사용하면 model 은 flux1-dev 모델 밖에 쓸 수 없음.

 

 

3-1. worker-comfyui 포크 뜨기

worker-comfyui 에서 제공하는 Dockerfile, start.sh, 등 Network Volume 을 연동하기 위해 커스텀이 필요하다

 

Dockerfile 을 보면, MODEL_TYPE 에 따라서, 어떤 모델을 미리 다운로드 받을지 정한다. 

나는 이미 필요한 모델들은 Network Volume 에 있기 때문에,

docker build 할때 arg 로 MODEL_TYPE=none 으로 넘기면 된다.

(이건 나중에 docker build 할때 매개변수 넘기는 명령어 세팅함)

 

 

3-2. Network Volume 내부 models 디렉토리 연결하기

Runpod - Pod 서비스에 Network Volume 에서 ComfyUI 를 올려서 사용할때,

내부적으로 파일시스템 구조가 아래와 같이 세팅 되어있다.

/workspace/ComfyUI/models/ 폴더 아래에, 

/checkpoints, /loras, /diffusion_models... 등등 모델타입별로 모델들이 들어있다.

 

이 구조를 내가 사용할 comfyui 어플리케이션에게 알려주어야 한다.

 

포크 뜬 worker-comfyui 레포지토리 안에, 

 

/src/extra_model_paths.yaml 파일을 만들고, 아래와 같이 매핑 세팅을 한다.

runpod_worker_comfy:
  base_path: /runpod-volume/ComfyUI/models
  checkpoints: checkpoints/
  clip: clip/
  clip_vision: clip_vision/
  configs: configs/
  controlnet: controlnet/
  embeddings: embeddings/
  loras: loras/
  upscale_models: upscale_models/
  vae: vae/
  unet: unet/
  diffusion_models: diffusion_models/
  text_encoders: text_encoders/
  diffusers: diffusers/
  gligen: gligen/
  hypernetworks: hypernetworks/
  photomaker: photomaker/
  pulid: pulid/
  style_models: style_models/
  vae_approx: vae_approx/
  wget-log: wget-log/

 

 

3-3. Network Volume 내부 custom_nodes 디렉토리 연결하기

/srt/start.sh  수정

 

시작 쉘 파일에, 사용하던 Network Volume 안의 custom_nodes 를 worker-comfyui 상의 custom_nodes 참조 폴더 안에 복사를 한다

(심볼릭 링크로 처리를 하려했었는데, custom_nodes 를 불러오지 못하는 이슈가 있어서, 통째로 복사하는 방식을 택함)

 

# ─────────────────────────────────────────────────────────────
# Debug: Check if storage paths exist
echo "worker-comfyui: Checking storage paths..."

# Copy network-volume ComfyUI resources into installed ComfyUI
echo "worker-comfyui: Copying custom_nodes from Network Volume..."

# 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
# ─────────────────────────────────────────────────────────────

 

 

 

3-4. custom_nodes 에서 필요한 PIP requirements 세팅

 

/requirements-custom-nodes.txt

 

파일을 생성해서, 필요한 requirements 들을 따로 관리하도록 한다.

여기 적힌 의존성을 읽어서, Dockerfile 에서 설치를 하도록 해야한다

 

# 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
# diffusers
# matplotlib
# pillow
# requests
# websocket-client
# lark
# scipy
# numpy
# pandas

 

 

Dockerfile 수정

# 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

# 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

 

 

4. 커스텀한 docker 이미지를 docker hub 에 업로드

.github/workflows/docker-build.yml

 

생성하고, 아까 위에서 언급했던 docker build 할때, MODEL_TYPE=none

로 변수를 넘겨서 빌드할도록 세팅한다.

 

(Github Action 으로 docker 이미지를 build 해서 Docker hub 에 올리는 방법을 사용했지만, 

Runpod 에서 Docker Hub 대신, Github Repo 를 통해서 서비스를 올리는 방법도 제공을 한다.

 

지금 소개하는 Docker Hub 에 올리고 Runpod 에서 그 이미지로 배포하는 방식은, 이미지를 수정해서 Docker 이미지 버전이 변경되었을때, 자동으로 roll out 이 되지 않아서, 버전을 수정해주어야한다.

 

Github Repo 로 세팅을 하면 이미지를 변경해서 레포지토리에 push 되면, roll-out 이 트리거 되어서, 신규 버전으로 배포가 가능하다)

 

# .github/workflows/docker-build.yml
name: Build and Push Docker Image

on:
  push:
    branches:
      - main

jobs:
  build_and_push:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          platforms: linux/amd64
          no-cache: true
          tags: |
            ${{ secrets.DOCKERHUB_USERNAME }}/worker-comfyui:${{ github.run_id }}
            ${{ secrets.DOCKERHUB_USERNAME }}/worker-comfyui:latest
          build-args: |
            MODEL_TYPE=none

 

 

Docker Hub 에 이미지 배포가 완료되면

 

1번에서 세팅한 docker Image 이름을 본인 docker 계정 밑에, 본인이 설정한 repo 이름으로 설정하여 배포한다.
DOCKERHUB_USERNAME/worker-comfyui 

 

 

 

5. Next.js 웹 어플리케이션에서, Serverless Endpoint 로 API 요청

Runpod 자체에 Request 로 workflow 를 담아서 요청 테스트도 가능하다

 

이제, Next.js 에서 요청을 날려보려한다.

 

브라우저에서 바로 fetch를 하면 CORS 가 발생하므로,

Next 의 api/route.ts 라우트 함수를 사용해서 Next 서버에서 요청을 날리고 응답을 받는 방식으로 구성하였다 (백엔드 구성을 생략)

 

/app/api/runpod/route.ts 를 생성한다

 

import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  try {
    const formData = await request.formData();
    const imageFile = formData.get("image") as File;

    if (!imageFile) {
      return NextResponse.json({ error: "Image is required" }, { status: 400 });
    }

    const allowedTypes = ["image/png", "image/jpeg", "image/jpg"];
    if (!allowedTypes.includes(imageFile.type)) {
      return NextResponse.json(
        { error: "Only PNG, JPG, and JPEG files are allowed" },
        { status: 400 }
      );
    }

    if (!process.env.RUNPOD_API_KEY) {
      return NextResponse.json(
        { error: "RUNPOD_API_KEY is not configured" },
        { status: 500 }
      );
    }

    const imageBytes = await imageFile.arrayBuffer();
    const imageBase64 = Buffer.from(imageBytes).toString("base64");

    const payload = {
      input: {
        workflow: {
          "1": {
            inputs: {
              vae_name: "ae.safetensors",
            },
            class_type: "VAELoader",
            _meta: {
              title: "Load VAE",
            },
          },
          "2": {
            inputs: {
              clip_name1: "clip_l.safetensors",
              clip_name2: "t5xxl_fp8_e4m3fn_scaled.safetensors",
              type: "flux",
              device: "default",
            },
            class_type: "DualCLIPLoader",
            _meta: {
              title: "DualCLIPLoader",
            },
          },
          "3": {
            inputs: {
              image: ["4", 0],
            },
            class_type: "FluxKontextImageScale",
            _meta: {
              title: "FluxKontextImageScale",
            },
          },
          "4": {
            inputs: {
              direction: "right",
              match_image_size: true,
              spacing_width: 0,
              spacing_color: "white",
              image1: ["20", 0],
            },
            class_type: "ImageStitch",
            _meta: {
              title: "Image Stitch",
            },
          },
          "12": {
            inputs: {
              unet_name: "flux1-dev-kontext_fp8_scaled.safetensors",
              weight_dtype: "default",
            },
            class_type: "UNETLoader",
            _meta: {
              title: "Load Diffusion Model",
            },
          },
          "14": {
            inputs: {
              pixels: ["3", 0],
              vae: ["1", 0],
            },
            class_type: "VAEEncode",
            _meta: {
              title: "VAE Encode",
            },
          },
          "15": {
            inputs: {
              samples: ["25", 0],
              vae: ["1", 0],
            },
            class_type: "VAEDecode",
            _meta: {
              title: "VAE Decode",
            },
          },
          "16": {
            inputs: {
              conditioning: ["24", 0],
            },
            class_type: "ConditioningZeroOut",
            _meta: {
              title: "ConditioningZeroOut",
            },
          },
          "19": {
            inputs: {
              lora_name: "kontext-mk-glasses-v3.safetensors",
              strength_model: 0.9000000000000001,
              strength_clip: 1,
              model: ["12", 0],
              clip: ["2", 0],
            },
            class_type: "LoraLoader",
            _meta: {
              title: "Load LoRA",
            },
          },
          "20": {
            inputs: {
              image: "input_image.png",
              refresh: "refresh",
            },
            class_type: "LoadImageOutput",
            _meta: {
              title: "Load Image (from Outputs)",
            },
          },
          "21": {
            inputs: {
              images: ["3", 0],
            },
            class_type: "PreviewImage",
            _meta: {
              title: "Preview Image",
            },
          },
          "22": {
            inputs: {
              filename_prefix: "ComfyUI",
              images: ["15", 0],
            },
            class_type: "SaveImage",
            _meta: {
              title: "Save Image",
            },
          },
          "23": {
            inputs: {
              text: "make him wear alio_glasses",
              clip: ["19", 1],
            },
            class_type: "CLIPTextEncode",
            _meta: {
              title: "CLIP Text Encode (Positive Prompt)",
            },
          },
          "24": {
            inputs: {
              text: "",
              clip: ["19", 1],
            },
            class_type: "CLIPTextEncode",
            _meta: {
              title: "CLIP Text Encode (Negative Prompt)",
            },
          },
          "25": {
            inputs: {
              seed: 945612421605210,
              steps: 20,
              cfg: 0.8,
              sampler_name: "euler",
              scheduler: "simple",
              denoise: 1,
              model: ["19", 0],
              positive: ["27", 0],
              negative: ["16", 0],
              latent_image: ["14", 0],
            },
            class_type: "KSampler",
            _meta: {
              title: "KSampler",
            },
          },
          "26": {
            inputs: {
              conditioning: ["23", 0],
              latent: ["14", 0],
            },
            class_type: "ReferenceLatent",
            _meta: {
              title: "ReferenceLatent",
            },
          },
          "27": {
            inputs: {
              guidance: 3,
              conditioning: ["26", 0],
            },
            class_type: "FluxGuidance",
            _meta: {
              title: "FluxGuidance",
            },
          },
        },
        images: [
          {
            name: "input_image.png",
            image: imageBase64,
          },
        ],
      },
    };

    const response = await fetch(
      "https://api.runpod.ai/v2/[YOUR_ENDPOINT]/run",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${process.env.RUNPOD_API_KEY}`,
        },
        body: JSON.stringify(payload),
      }
    );

    if (!response.ok) {
      const errorText = await response.text();
      console.error(`RunPod API error: ${response.status} - ${errorText}`);

      if (response.status === 401) {
        throw new Error(
          "Authentication failed. Please check your RunPod API token."
        );
      }

      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    return NextResponse.json(data);
  } catch (error) {
    console.error("Error in Glasses LORA API:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}

 

 

여기서 핵심은 아래와 같다.

 

  • Kontext 모델도 사용가능 
  • Kontext 에 input 으로 사용할 이미지를 request 로 담아서 요청할 예정, 이를 받아서, workflow 상에 담아서 /run 요청가능
  • RUNPOD_API_KEY 는 runpod 에서 api 키를 생성해서 요청시 authorization 헤더에 Bearer 로 담아주어야 하므로 필요
  • 이미지는 base64로 변환해서 전송해야함
  • ComfyUI 에서 사용하던 워크플로우를 EXPORT(API) 형식으로 추출하여, 나오는 json 을 그대로, workflow 에 담으면 된다
  • json을 보면, 알 수 있듯이, Network Volume 에서 다운 받아서 기존에 사용하던 kontext 양자화 모델이나 내가 만든 Lora 들을 그대로 사용 할 수 있다.
  • 그 다음 이제 중요하게 맞춰 주어야 하는게 , input 으로 받는 image 의 name 값이다.
    20 번 노드에, inputs.image : “input_image.png”
    여기서 사용한, 이미지 명을
    맨 하단에 images 리스트에, name 과 맞춰 주어야한다.
  • 워크플로우 상에서 이미지를 여러개 로드하면, 각각 이름을 맞추고, images 리스트에 담으면 된다.

 

6. 컴포넌트 단에서 api 함수 호출

  const handleSubmit = async () => {
    if (!selectedImage) {
      alert("이미지를 선택해주세요.");
      return;
    }

    setIsLoading(true);
    setResult(null);
    setStartTime(Date.now());
    setElapsedTime(0);
    setStatusMessage("이미지를 업로드하고 처리 중...");

    try {
      const formData = new FormData();
      formData.append("image", selectedImage);

      const response = await fetch("/api/runpod", {
        method: "POST",
        body: formData,
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.error || "API 요청 실패");
      }

      const data = await response.json();
      
      if (data.id) {
        setJobId(data.id);
        setStatusMessage("작업이 시작되었습니다. 상태를 확인하는 중...");
        await pollJobStatus(data.id);
      } else {
        throw new Error("Job ID를 받지 못했습니다.");
      }
    } catch (error) {
      console.error("Submit error:", error);
      setStatusMessage(`오류: ${error instanceof Error ? error.message : "알 수 없는 오류"} (약 ${elapsed}초)`);
      setIsLoading(false);
    }
  };

 

 

작업의 진행상황은 runpod 에서 제공하는 "/status"  엔드포인트로, 폴링 방식으로 N초에 한번씩 jobId 를 담아서 요청하면,

status 상태를 볼수 있고, 완료시 결과이미지가 base64 스트링 형태로 응답된다.

 

이를 화면에 렌더링하거나, 더 나아가 S3에 업로드하여 output 을 관리하면 됨

 

 

 

[변경이 발생할 수 있는 포인트들]

✅ workflow 는 프론트 단에서 변경해서 요청가능 -> 필요시 text 나, json 형식으로 로 받아서 요청하도록 구현하면 됨

✅ 필요한 모델을 Network Volume 에 다운 받으면, 새로이 docker image build 나 배포 없어도 즉시 workflow 상에서 모델 사용가능

 

 

 

 

 

1000장의 데이터

batch_size  = 10 

update : 100번

장점: 미세한 조정 가능

단점: 속도가 느리다 

업데이트 되는 양 자체도 작아서 초기 학습 값이 잘못되었을때 다시 되돌아 오지 못한다.

 

batch_size = 100

update : 10번

장점: 크게크게 업데이트

속도가 빠르다

 

단점: 정밀도 떨어진다

 

적절한 batch_size 찾는 법

model.fit(batch_size는 32로 디폴트 지정됨)

또는 전처리 단계에서 batch_size 설정 가능 -> 장점은 train batch 크게/ valid batch 작게 따로 설정가능

 

Train/valid data 개수 batch_size steps_per_epoch  validation_steps
1000 10 100  
200 10 20  
1024 10 1024//10 + 1  

 

반드시 정수로 나누어 떨어지게 값을 조정해야한다.

그렇지 못할 경우에는 몫에 + 1 로 값을 입력한다.

 

전체 복사 붙여넣게 금지

xs 와 ys 값이 랜덤으로 주어지므로 

#YOUR CODE HERE  

이하로만 손대기

 

import numpy as np
import tensorflow as tf


def solution_model():
    xs = np.array([-1.0, 0.0, 1.0, 2.0, 3.0, 4.0], dtype=float)
    ys = np.array([5.0, 6.0, 7.0, 8.0, 9.0, 10.0], dtype=float)
    # YOUR CODE HERE
    model = tf.keras.models.Sequential([
        tf.keras.layers.Dense(1, input_shape=[1]),
    ])
    model.compile(optimizer="sgd", loss="mse")
    model.fit(xs, ys, epochs=500, verbose=0)
    return model


# Note that you'll need to save your model as a .h5 like this
# This .h5 will be uploaded to the testing infrastructure
# and a score will be returned to you
if __name__ == '__main__':
    model = solution_model()
    model.save("mymodel.h5")
    print(model.predict([10.0]))

ctrl + m + a

위에 셀 추가

 

ctrl + m + b

아래에 셀 추가

 

ctrl + m + d

셀 삭제

 

ctrl + enter

해당 셀 실행

 

shift + enter

해당 셀 실행 후 다음 셀로 이동

 

Alt + enter

해당 셀 실행 후 아래쪽에 셀 추가

 

ctrl + m + m

해당 셀을 텍스트셀로 변환

 

ctrl + m + y

해당 셀을 코드셀로 변환

 

+ Recent posts