Compare commits

...

25 Commits

Author SHA1 Message Date
B. Bergeron
9cac14f842
Merge fa71050a07 into 8aabe2403e 2026-01-31 00:50:30 +01:00
pythongosssss
8aabe2403e
Add color type and Color to RGB Int node (#12145)
Some checks are pending
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
* add color type and color to rgb int node

* review fix for allowing output

---------

Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-01-30 15:01:33 -08:00
Alexander Piskun
0167653781
feat(api-nodes): add RecraftCreateStyleNode node (#12055)
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-01-30 14:04:43 -08:00
Jedrzej Kosinski
0a7993729c
Remove NodeInfoV3-related code; we are almost 100% guaranteed to stick with NodeInfoV1 for the foreseable future (#12147)
Co-authored-by: guill <jacob.e.segal@gmail.com>
2026-01-30 10:21:48 -08:00
B. Bergeron
fa71050a07
Split data volume 2026-01-10 19:03:47 -05:00
B. Bergeron
7795a4e86c
Improve documentation regarding pip install step 2026-01-10 18:55:37 -05:00
B. Bergeron
c4c388ffc8
Improve documentation regarding numerical ownership 2026-01-10 18:55:37 -05:00
B. Bergeron
c804c0c12e
Always update Python dependencies + don't hide pip logs 2026-01-10 18:55:37 -05:00
B. Bergeron
6c9110564b
Pin base image version to 3.12.12-trixie + document version choice 2026-01-10 16:58:21 -05:00
B. Bergeron
b4dcbdfac7
Remove unused "AS" docker statement 2026-01-10 14:02:52 -05:00
B. Bergeron
2c859e9558
Remove superfluous interface binding 2026-01-10 14:02:52 -05:00
B. Bergeron
174d91c9ed
Improved documentation 2026-01-10 14:02:52 -05:00
B. Bergeron
e1cf4f7420
Don't rebuild whole image when APT_EXTRA_PACKAGES changes 2026-01-10 14:02:51 -05:00
B. Bergeron
357f89a4bf
Fix permission issue on legacy builds 2026-01-10 14:02:51 -05:00
B. Bergeron
477f330415
Use stable apt-get CLI interface instead of apt 2026-01-10 14:02:51 -05:00
B. Bergeron
36e19df686
Use recommended compose file name for Docker Compose 2026-01-10 14:02:51 -05:00
B. Bergeron
aba97d6ada
Add @bbergeron0 to CODEOWNER 2026-01-10 14:02:51 -05:00
B. Bergeron
7419345b76
Remove superfluous command-separators 2026-01-10 14:02:51 -05:00
B. Bergeron
41b4c3ea73
Force LF eol for entrypoint.sh 2026-01-10 14:02:51 -05:00
B. Bergeron
5b27c661c6
Update ownership of /comfyui to comfyui user 2026-01-10 14:02:51 -05:00
B. Bergeron
6572cbb61d
Inform user that installation might take a while 2026-01-10 14:02:50 -05:00
B. Bergeron
4f12985e45
Install extra system dependencies at build-time 2026-01-10 14:02:50 -05:00
B. Bergeron
847e3cc3a2
Persist models installed by model managers 2026-01-10 14:02:50 -05:00
B. Bergeron
e7ebda4b61
Add instructions for Docker installation 2026-01-10 14:02:48 -05:00
B. Bergeron
eeee0f5b1b
Add local Docker support 2026-01-10 14:00:21 -05:00
12 changed files with 413 additions and 85 deletions

31
.dockerignore Normal file
View File

@ -0,0 +1,31 @@
# This file should remain in sync with .gitignore. If you need to make changes,
# please add a comment explaining why. For items that must be removed, comment
# them out instead of deleting them.
__pycache__/
*.py[cod]
/output/
/input/
# This file prevents the image from building and would be overwritten by the
# /data volume in any case.
#!/input/example.png
/models/
/temp/
/custom_nodes/
!custom_nodes/example_node.py.example
extra_model_paths.yaml
/.vs
.vscode/
.idea/
venv/
.venv/
/web/extensions/*
!/web/extensions/logging.js.example
!/web/extensions/core/
/tests-ui/data/object_info.json
/user/
*.log
web_custom_versions/
.DS_Store
openapi.yaml
filtered-openapi.yaml
uv.lock

3
.gitattributes vendored
View File

@ -1,3 +1,6 @@
/web/assets/** linguist-generated
/web/** linguist-vendored
comfy_api_nodes/apis/__init__.py linguist-generated
# Force LF eol for Docker entrypoint (fix "exec: no such file or directory"
# error with CRLF checkouts)
entrypoint.sh text eol=lf

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
# If you modify this file, remember to update .dockerignore as well.
__pycache__/
*.py[cod]
/output/

85
Dockerfile Normal file
View File

@ -0,0 +1,85 @@
# Docker buildfile for the ComfyUI image, with support for hardware
# acceleration, file ownership synchronization, custom nodes, and custom node
# managers.
# While Python 3.13 is well supported by ComfyUI, some older custom node packs
# may not work correctly with this version, which is why we're staying on Python
# 3.12 for now.
#
# Users are free to try different base Python image tags (e.g., 3.13, alpine,
# *-slim), but for maintainability, only one base version is officially
# supported at a time.
FROM python:3.12.12-trixie
# Install cmake, which is an indirect installation dependencies
RUN apt-get update && apt-get install -y --no-install-recommends cmake
# Create a regular user whose UID and GID will match the host user's at runtime.
# Also create a home directory for this user (-m), as some common Python tools
# (such as uv) interact with the users home directory.
RUN useradd -m comfyui
# Install ComfyUI under /comfyui and set folder ownership to the comfyui user.
# With the legacy Docker builder (DOCKER_BUILDKIT=0), WORKDIR always creates missing
# directories as root (even if a different USER is active). To ensure the comfyui user
# can write inside, ownership must be fixed manually.
WORKDIR /comfyui
RUN chown comfyui:comfyui .
# Install ComfyUI as ComfyUI
USER comfyui
# Set up a Python virtual environment and configure it as the default Python.
#
# Reasons for using a virtual environment:
# - Some custom nodes use third-party tools like uv, which do not support
# user-level installations.
# - Custom node managers may install or update dependencies as the regular user,
# so a global installation is not an option.
# This leaves virtual environments as the only viable choice.
RUN python -m venv .venv
ENV PATH="/comfyui/.venv/bin:$PATH"
# Install ComfyUI's Python dependencies. Although dependency keeping is also
# performed at startup, building ComfyUI's base dependencies into the image
# significantly speeds up each containers' first run.
#
# Since this step takes a long time to complete, it's performed early to take
# advantage of Docker's build cache, thereby accelerating subsequent builds.
COPY requirements.txt manager_requirements.txt ./
RUN pip install --no-cache-dir --disable-pip-version-check \
-r requirements.txt
# Install ComfyUI
COPY . .
# Purely declarative: inform Docker and image users that this image is designed
# to listen on port 8188 for the web GUI.
EXPOSE 8188
# Declare persistent volumes. We assign one volume per data directory to match
# ComfyUIs natural file layout and to let users selectively choose which
# directories they want to mount.
VOLUME /comfyui/.venv
VOLUME /comfyui/custom_nodes
VOLUME /comfyui/input
VOLUME /comfyui/models
VOLUME /comfyui/output
VOLUME /comfyui/temp
VOLUME /comfyui/user
VOLUME /home/comfyui
# Switch back to root to run the entrypoint and to install additional system
# dependencies
USER root
# Configure entrypoint
RUN chmod +x entrypoint.sh
ENTRYPOINT [ "./entrypoint.sh" ]
CMD [ "python", "./main.py" ]
# Install additional system dependencies
ARG APT_EXTRA_PACKAGES
RUN apt-get install -y --no-install-recommends $APT_EXTRA_PACKAGES \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

View File

@ -46,6 +46,12 @@ ComfyUI lets you design and execute advanced stable diffusion pipelines using a
- Get the latest commits and completely portable.
- Available on Windows.
#### [Docker Install](#running-with-docker)
- Run ComfyUI inside an isolated Docker container
- Most secure way to run ComfyUI and custom node packs
- Requires Docker and Docker Compose
- Supports NVIDIA GPUs (Not tested on other hardware.)
#### [Manual Install](#manual-install-windows-linux)
Supports all operating systems and GPU types (NVIDIA, AMD, Intel, Apple Silicon, Ascend).
@ -350,6 +356,28 @@ For models compatible with Iluvatar Extension for PyTorch. Here's a step-by-step
| `--enable-manager-legacy-ui` | Use the legacy manager UI instead of the new UI (requires `--enable-manager`) |
| `--disable-manager-ui` | Disable the manager UI and endpoints while keeping background features like security checks and scheduled installation completion (requires `--enable-manager`) |
## Running with Docker
Start by installing Docker, Docker Compose, and the NVIDIA Container Toolkit on
your host. Next, edit `compose.yaml` and update the `UID` and `GID` variables to
match your user. Additional fields are documented in the file for further
customization.
Once ready, build and run the image locally:
```shell
# (Re)build the Docker image. Run this before the first start, after updating
# ComfyUI, or after changing any build arguments in `compose.yaml`.
docker compose build
# Start ComfyUI. This reuses the most recently built image.
docker compose up
```
To stop and remove the container along with its volumes, run:
```shell
docker compose down -v
```
# Running

View File

@ -1146,6 +1146,20 @@ class ImageCompare(ComfyTypeI):
def as_dict(self):
return super().as_dict()
@comfytype(io_type="COLOR")
class Color(ComfyTypeIO):
Type = str
class Input(WidgetInput):
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None,
socketless: bool=True, advanced: bool=None, default: str="#ffffff"):
super().__init__(id, display_name, optional, tooltip, None, default, socketless, None, None, None, None, advanced)
self.default: str
def as_dict(self):
return super().as_dict()
DYNAMIC_INPUT_LOOKUP: dict[str, Callable[[dict[str, Any], dict[str, Any], tuple[str, dict[str, Any]], str, list[str] | None], None]] = {}
def register_dynamic_input_func(io_type: str, func: Callable[[dict[str, Any], dict[str, Any], tuple[str, dict[str, Any]], str, list[str] | None], None]):
DYNAMIC_INPUT_LOOKUP[io_type] = func
@ -1252,23 +1266,6 @@ class NodeInfoV1:
price_badge: dict | None = None
search_aliases: list[str]=None
@dataclass
class NodeInfoV3:
input: dict=None
output: dict=None
hidden: list[str]=None
name: str=None
display_name: str=None
description: str=None
python_module: Any = None
category: str=None
output_node: bool=None
deprecated: bool=None
experimental: bool=None
dev_only: bool=None
api_node: bool=None
price_badge: dict | None = None
@dataclass
class PriceBadgeDepends:
@ -1497,40 +1494,6 @@ class Schema:
)
return info
def get_v3_info(self, cls) -> NodeInfoV3:
input_dict = {}
output_dict = {}
hidden_list = []
# TODO: make sure dynamic types will be handled correctly
if self.inputs:
for input in self.inputs:
add_to_dict_v3(input, input_dict)
if self.outputs:
for output in self.outputs:
add_to_dict_v3(output, output_dict)
if self.hidden:
for hidden in self.hidden:
hidden_list.append(hidden.value)
info = NodeInfoV3(
input=input_dict,
output=output_dict,
hidden=hidden_list,
name=self.node_id,
display_name=self.display_name,
description=self.description,
category=self.category,
output_node=self.is_output_node,
deprecated=self.is_deprecated,
experimental=self.is_experimental,
dev_only=self.is_dev_only,
api_node=self.is_api_node,
python_module=getattr(cls, "RELATIVE_PYTHON_MODULE", "nodes"),
price_badge=self.price_badge.as_dict(self.inputs) if self.price_badge is not None else None,
)
return info
def get_finalized_class_inputs(d: dict[str, Any], live_inputs: dict[str, Any], include_hidden=False) -> tuple[dict[str, Any], V3Data]:
out_dict = {
"required": {},
@ -1585,9 +1548,6 @@ def add_to_dict_v1(i: Input, d: dict):
as_dict.pop("optional", None)
d.setdefault(key, {})[i.id] = (i.get_io_type(), as_dict)
def add_to_dict_v3(io: Input | Output, d: dict):
d[io.id] = (io.get_io_type(), io.as_dict())
class DynamicPathsDefaultValue:
EMPTY_DICT = "empty_dict"
@ -1748,13 +1708,6 @@ class _ComfyNodeBaseInternal(_ComfyNodeInternal):
# set hidden
type_clone.hidden = HiddenHolder.from_v3_data(v3_data)
return type_clone
@final
@classmethod
def GET_NODE_INFO_V3(cls) -> dict[str, Any]:
schema = cls.GET_SCHEMA()
info = schema.get_v3_info(cls)
return asdict(info)
#############################################
# V1 Backwards Compatibility code
#--------------------------------------------
@ -2099,6 +2052,7 @@ __all__ = [
"AnyType",
"MultiType",
"Tracks",
"Color",
# Dynamic Types
"MatchType",
"DynamicCombo",
@ -2107,12 +2061,10 @@ __all__ = [
"HiddenHolder",
"Hidden",
"NodeInfoV1",
"NodeInfoV3",
"Schema",
"ComfyNode",
"NodeOutput",
"add_to_dict_v1",
"add_to_dict_v3",
"V3Data",
"ImageCompare",
"PriceBadgeDepends",

View File

@ -1,11 +1,8 @@
from __future__ import annotations
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field, conint, confloat
from pydantic import BaseModel, Field
class RecraftColor:
@ -229,24 +226,24 @@ class RecraftColorObject(BaseModel):
class RecraftControlsObject(BaseModel):
colors: Optional[list[RecraftColorObject]] = Field(None, description='An array of preferable colors')
background_color: Optional[RecraftColorObject] = Field(None, description='Use given color as a desired background color')
no_text: Optional[bool] = Field(None, description='Do not embed text layouts')
artistic_level: Optional[conint(ge=0, le=5)] = Field(None, description='Defines artistic tone of your image. At a simple level, the person looks straight at the camera in a static and clean style. Dynamic and eccentric levels introduce movement and creativity. The value should be in range [0..5].')
colors: list[RecraftColorObject] | None = Field(None, description='An array of preferable colors')
background_color: RecraftColorObject | None = Field(None, description='Use given color as a desired background color')
no_text: bool | None = Field(None, description='Do not embed text layouts')
artistic_level: int | None = Field(None, description='Defines artistic tone of your image. At a simple level, the person looks straight at the camera in a static and clean style. Dynamic and eccentric levels introduce movement and creativity. The value should be in range [0..5].')
class RecraftImageGenerationRequest(BaseModel):
prompt: str = Field(..., description='The text prompt describing the image to generate')
size: Optional[RecraftImageSize] = Field(None, description='The size of the generated image (e.g., "1024x1024")')
n: conint(ge=1, le=6) = Field(..., description='The number of images to generate')
negative_prompt: Optional[str] = Field(None, description='A text description of undesired elements on an image')
model: Optional[RecraftModel] = Field(RecraftModel.recraftv3, description='The model to use for generation (e.g., "recraftv3")')
style: Optional[str] = Field(None, description='The style to apply to the generated image (e.g., "digital_illustration")')
substyle: Optional[str] = Field(None, description='The substyle to apply to the generated image, depending on the style input')
controls: Optional[RecraftControlsObject] = Field(None, description='A set of custom parameters to tweak generation process')
style_id: Optional[str] = Field(None, description='Use a previously uploaded style as a reference; UUID')
strength: Optional[confloat(ge=0.0, le=1.0)] = Field(None, description='Defines the difference with the original image, should lie in [0, 1], where 0 means almost identical, and 1 means miserable similarity')
random_seed: Optional[int] = Field(None, description="Seed for video generation")
size: RecraftImageSize | None = Field(None, description='The size of the generated image (e.g., "1024x1024")')
n: int = Field(..., description='The number of images to generate')
negative_prompt: str | None = Field(None, description='A text description of undesired elements on an image')
model: RecraftModel | None = Field(RecraftModel.recraftv3, description='The model to use for generation (e.g., "recraftv3")')
style: str | None = Field(None, description='The style to apply to the generated image (e.g., "digital_illustration")')
substyle: str | None = Field(None, description='The substyle to apply to the generated image, depending on the style input')
controls: RecraftControlsObject | None = Field(None, description='A set of custom parameters to tweak generation process')
style_id: str | None = Field(None, description='Use a previously uploaded style as a reference; UUID')
strength: float | None = Field(None, description='Defines the difference with the original image, should lie in [0, 1], where 0 means almost identical, and 1 means miserable similarity')
random_seed: int | None = Field(None, description="Seed for video generation")
# text_layout
@ -258,5 +255,13 @@ class RecraftReturnedObject(BaseModel):
class RecraftImageGenerationResponse(BaseModel):
created: int = Field(..., description='Unix timestamp when the generation was created')
credits: int = Field(..., description='Number of credits used for the generation')
data: Optional[list[RecraftReturnedObject]] = Field(None, description='Array of generated image information')
image: Optional[RecraftReturnedObject] = Field(None, description='Single generated image')
data: list[RecraftReturnedObject] | None = Field(None, description='Array of generated image information')
image: RecraftReturnedObject | None = Field(None, description='Single generated image')
class RecraftCreateStyleRequest(BaseModel):
style: str = Field(..., description="realistic_image, digital_illustration, vector_illustration, or icon")
class RecraftCreateStyleResponse(BaseModel):
id: str = Field(..., description="UUID of the created style")

View File

@ -12,6 +12,8 @@ from comfy_api_nodes.apis.recraft import (
RecraftColor,
RecraftColorChain,
RecraftControls,
RecraftCreateStyleRequest,
RecraftCreateStyleResponse,
RecraftImageGenerationRequest,
RecraftImageGenerationResponse,
RecraftImageSize,
@ -323,6 +325,75 @@ class RecraftStyleInfiniteStyleLibrary(IO.ComfyNode):
return IO.NodeOutput(RecraftStyle(style_id=style_id))
class RecraftCreateStyleNode(IO.ComfyNode):
@classmethod
def define_schema(cls):
return IO.Schema(
node_id="RecraftCreateStyleNode",
display_name="Recraft Create Style",
category="api node/image/Recraft",
description="Create a custom style from reference images. "
"Upload 1-5 images to use as style references. "
"Total size of all images is limited to 5 MB.",
inputs=[
IO.Combo.Input(
"style",
options=["realistic_image", "digital_illustration"],
tooltip="The base style of the generated images.",
),
IO.Autogrow.Input(
"images",
template=IO.Autogrow.TemplatePrefix(
IO.Image.Input("image"),
prefix="image",
min=1,
max=5,
),
),
],
outputs=[
IO.String.Output(display_name="style_id"),
],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
price_badge=IO.PriceBadge(
expr="""{"type":"usd","usd": 0.04}""",
),
)
@classmethod
async def execute(
cls,
style: str,
images: IO.Autogrow.Type,
) -> IO.NodeOutput:
files = []
total_size = 0
max_total_size = 5 * 1024 * 1024 # 5 MB limit
for i, img in enumerate(list(images.values())):
file_bytes = tensor_to_bytesio(img, total_pixels=2048 * 2048, mime_type="image/webp").read()
total_size += len(file_bytes)
if total_size > max_total_size:
raise Exception("Total size of all images exceeds 5 MB limit.")
files.append((f"file{i + 1}", file_bytes))
response = await sync_op(
cls,
endpoint=ApiEndpoint(path="/proxy/recraft/styles", method="POST"),
response_model=RecraftCreateStyleResponse,
files=files,
data=RecraftCreateStyleRequest(style=style),
content_type="multipart/form-data",
max_retries=1,
)
return IO.NodeOutput(response.id)
class RecraftTextToImageNode(IO.ComfyNode):
@classmethod
def define_schema(cls):
@ -395,7 +466,7 @@ class RecraftTextToImageNode(IO.ComfyNode):
negative_prompt: str = None,
recraft_controls: RecraftControls = None,
) -> IO.NodeOutput:
validate_string(prompt, strip_whitespace=False, max_length=1000)
validate_string(prompt, strip_whitespace=False, min_length=1, max_length=1000)
default_style = RecraftStyle(RecraftStyleV3.realistic_image)
if recraft_style is None:
recraft_style = default_style
@ -1024,6 +1095,7 @@ class RecraftExtension(ComfyExtension):
RecraftStyleV3DigitalIllustrationNode,
RecraftStyleV3LogoRasterNode,
RecraftStyleInfiniteStyleLibrary,
RecraftCreateStyleNode,
RecraftColorRGBNode,
RecraftControlsNode,
]

View File

@ -0,0 +1,42 @@
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io
class ColorToRGBInt(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="ColorToRGBInt",
display_name="Color to RGB Int",
category="utils",
description="Convert a color to a RGB integer value.",
inputs=[
io.Color.Input("color"),
],
outputs=[
io.Int.Output(display_name="rgb_int"),
],
)
@classmethod
def execute(
cls,
color: str,
) -> io.NodeOutput:
# expect format #RRGGBB
if len(color) != 7 or color[0] != "#":
raise ValueError("Color must be in format #RRGGBB")
r = int(color[1:3], 16)
g = int(color[3:5], 16)
b = int(color[5:7], 16)
return io.NodeOutput(r * 256 * 256 + g * 256 + b)
class ColorExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [ColorToRGBInt]
async def comfy_entrypoint() -> ColorExtension:
return ColorExtension()

46
compose.yaml Normal file
View File

@ -0,0 +1,46 @@
# Docker Compose file to run ComfyUI locally using Docker.
services:
comfyui:
container_name: comfyui
build:
context: .
args:
# Declare additional system dependencies for custom nodes
APT_EXTRA_PACKAGES:
ports:
- 8188:8188
# Optional: enable GPU access for hardware acceleration.
deploy:
resources:
reservations:
devices:
- capabilities: [gpu]
volumes:
- ./custom_nodes:/comfyui/custom_nodes
- ./models:/comfyui/models
# (Optional) Mount host ComfyUI data directories
#
#- ./input:/comfyui/input
#- ./output:/comfyui/output
#- ./temp:/comfyui/temp
#- ./user:/comfyui/user
environment:
# Overwrite the container user's UID and GID to match the host's. This
# allows files created by ComfyUI to be mounted on the host without
# permission issues.
UID: 1000
GID: 1000
# Declare additional Python packages to install. Useful when a custom node
# pack does not properly specify all its dependencies or relies on
# optional dependencies.
PIP_EXTRA_PACKAGES:
# Optional: Override the default command. In this case, configure ComfyUI to
# listen on all network interfaces (which is required when not using
# `network_mode=host`.)
command: python ./main.py --listen 0.0.0.0

62
entrypoint.sh Executable file
View File

@ -0,0 +1,62 @@
#!/bin/sh
# Entrypoint script for the ComfyUI Docker image.
set -e
user="comfyui"
user_group="$user"
# Allow users to specify a UID and GID matching their own, so files created
# inside the container retain the same numeric ownership when mounted on the
# host.
if [ -n "$UID" ] && [ -n "$GID" ]; then
echo "[entrypoint] Setting user UID and GID..."
usermod -u "$UID" "$user" > /dev/null
groupmod -g "$GID" "$user_group"
else
echo "[entrypoint] Missing UID or GID environment variables; keeping default values."
fi
# Changing a user's UID and GID revokes that user's access to files owned by the
# original UID/GID. To preserve access to runtime data, the ownership of those
# directories must be updated recursively so that their numeric owner matches
# the user's new UID and GID.
echo "[entrypoint] Changing directory ownership..."
chown -R "$user:$user_group" \
/comfyui \
/home/comfyui
# To use CUDA and other NVIDIA features, regular users must belong to the group
# that owns the /dev/nvidia* device files -- typically the video group.
#
# Known issue: Because these device files are mounted from the host system,
# there's no guarantee that the device's group ID will match the intended group
# inside the container. For example, the video group might be mapped to GID 27
# on the host, which corresponds to the sudo group in the python:3.12 image.
# This shouldn't cause major problems, and given the lack of a universal
# standard for system GIDs, there isn't much we can realistically change to
# address this issue.
echo "[entrypoint] Adding user to GPU device groups..."
for dev in /dev/nvidia*; do
group=$(ls -ld "$dev" | awk '{print $4}')
usermod -aG "$group" "$user"
done
# Install or update the Python dependencies defined by ComfyUI (or any installed
# custom node) and also install any user-defined dependencies specified in
# PIP_EXTRA_PACKAGES.
echo "[entrypoint] Updating Python dependencies..."
su -c "
pip install \\
--no-cache-dir \\
--disable-pip-version-check \\
-r requirements.txt \\
$(find custom_nodes -mindepth 2 -maxdepth 2 -type f -name requirements.txt -printf "-r '%p' ") \\
$PIP_EXTRA_PACKAGES
" comfyui \
|| echo "[entrypoint] Failed to install dependencies, starting anyway" >&2
# Run command as comfyui
echo "[entrypoint] Running command"
exec su -c "$*" comfyui

View File

@ -2432,7 +2432,8 @@ async def init_builtin_extra_nodes():
"nodes_wanmove.py",
"nodes_image_compare.py",
"nodes_zimage.py",
"nodes_lora_debug.py"
"nodes_lora_debug.py",
"nodes_color.py"
]
import_failed = []