ComfyUI 是一个基于节点的图形化界面,用于构建和运行深度学习模型,特别是在生成式 AI 领域。
通过 Docker 容器化部署 ComfyUI 可以简化安装和管理过程,同时确保环境的一致性,利于备份和回滚。
由于 ComfyUI 特性,各个节点都会单独安装第三方库,所以更新节点会导致环境不一致从而影响整体运行,利用 Docker 的备份机制就可以很好的回避这一点。
此次我的服务器使用的是 NVIDIA GPU,因此在安装过程中会涉及到 CUDA 的配置,配置如下:
- CPU:Intel Core i9-14900KF
- 显卡:NVIDIA RTX 4090 48G 版
- 内存:128GB
- 系统:Debian12
1. 准备
1.1. Docker 安装
Debian12 安装 Docker 如下:
# Add Docker's official GPG key:
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
测试安装:
sudo docker run hello-world
编辑 /etc/docker/daemon.json 文件,添加以下代理、ipv6 禁用以及 cuda 配置(代理可选):
{
"proxies": {
"http-proxy": "http://192.168.1.1:1080",
"https-proxy": "http://192.168.1.1:1080",
"no-proxy": "*.test.example.com,.example.org"
},
"ipv6": false,
"runtimes": {
"nvidia": {
"args": [],
"path": "nvidia-container-runtime"
}
}
}
重启 Docker 服务已生效:
sudo systemctl daemon-reload
sudo systemctl restart docker
1.2. CUDA 安装
访问 [NVIDIA CUDA Toolkit Download](https://developer.nvidia.com/cuda-downloads 下载对应版本的 CUDA 安装包,安装过程请参考官网说明。
下面是在 debian12 上安装 CUDA 13.1 的步骤:
wget https://developer.download.nvidia.com/compute/cuda/13.1.0/local_installers/cuda-repo-debian12-13-1-local_13.1.0-590.44.01-1_amd64.deb
sudo dpkg -i cuda-repo-debian12-13-1-local_13.1.0-590.44.01-1_amd64.deb
sudo cp /var/cuda-repo-debian12-13-1-local/cuda-*-keyring.gpg /usr/share/keyrings/
sudo apt-get update
sudo apt-get -y install cuda-toolkit-13-1
apt-get install -y nvidia-container-toolkit
在安装完成后,验证 CUDA 是否安装成功:
nvcc -V
nvidia-smi
验证 Docker 是否可以使用 GPU:
docker run --rm --gpus all pytorch/pytorch:2.1.0-cuda11.8-cudnn8-runtime nvidia-smi
2. ComfyUI
2.1. 架构设计
ComfyUI 的 Docker 容器设计考虑到模型较大,以及更新和备份的需求,其主目录采用了以下目录结构:
$ tree -L 1
.
├── compose.yaml
├── Dockerfile
├── etc
├── logs
├── root
└── supervisor
compose.yaml 内容如下:
services:
comfyui:
container_name: comfyui
hostname: 4090-comfyui
build:
context: .
args:
http_proxy: ${PROXIES}
https_proxy: ${PROXIES}
image: comfyui:latest
restart: unless-stopped
ports:
- "8188:8188" # ComfyUI 端口映射
- "2222:22" # SSH 端口映射
environment:
- TZ=Asia/Shanghai
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=all
- SAGEATTN_FORCE_SM90=1
- http_proxy=${PROXIES}
- https_proxy=${PROXIES}
- HTTP_PROXY=${PROXIES}
- HTTPS_PROXY=${PROXIES}
runtime: nvidia
volumes:
- /mnt/sda/comfyui/comfyui:/comfy-ui
- /mnt/sda/comfyui/models:/comfy-ui/models
- /mnt/sda/comfyui/truetype:/usr/share/fonts/truetype
- ./root:/root
- ./etc:/etc
- ./logs:/var/log/supervisor
- ./supervisor:/etc/supervisor/conf.d
stdin_open: true
tty: true
解释:
- .env: 用于存放环境变量,如代理设置
PROXIES。 - 2222 和 8188 端口映射: 2222 用于 SSH 连接,8188 用于 ComfyUI 的 Web 界面访问。
- 卷挂载: 将主机的
/mnt/sda/comfyui/comfyui目录挂载到容器的/comfy-ui,用于存放 ComfyUI 的核心代码文件。/mnt/sda/comfyui/models用于存放模型文件,方便模型的管理和更新。/mnt/sda/comfyui/truetype用于存放字体文件,确保 ComfyUI 能够正确渲染文本。 - supervisor 和 logs 文件夹: 因 Docker 容器本身缺少
systemd,使用 supervisor 用于管理容器内的进程,实现日志管理、进程监控等功能。 - root 和 etc 文件夹: 用于存放用户和系统的配置文件,确保容器内的环境配置符合需求。
Dockerfile 文件如下:
FROM nvcr.io/nvidia/pytorch:25.01-py3
ENV TZ=Asia/Shanghai
# ==== 1) 基础依赖 ====
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3 python3-venv python3-pip \
build-essential git wget curl ca-certificates \
openssh-server supervisor \
ffmpeg zsh libpython3.12\
libgl1 libglib2.0-0 libsm6 libxrender1 libxext6 \
&& rm -rf /var/lib/apt/lists/*
# 让 python/pip 指向 3.x
RUN update-alternatives --install /usr/bin/python python /usr/bin/python3 1 && \
update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1
# ==== 2) 工作区 ====
WORKDIR /workspace
# ==== 3) (可选)SageAttention 示例:需要时解开注释即可 ====
# ARG SAGEATTENTION_BRANCH=main
# RUN git clone --depth 1 -b ${SAGEATTENTION_BRANCH} https://github.com/thu-ml/SageAttention.git && \
# cd SageAttention && uv pip install -v .
RUN mkdir -p /run/sshd && \
chmod 0755 /run/sshd
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf"]
supervisor 文件夹内有配置文件 sshd.conf 内容如下:
[program:sshd]
command=/usr/sbin/sshd -D
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/%(program_name)s.log
stdout_logfile_maxbytes=16MB
redirect_stderr=true
user=root
2.2. 构建镜像
在包含 Dockerfile 和 compose.yaml 的目录下,运行以下命令构建并启动容器:
docker compose build
如不出意外,容器可以正常构建成功,接着临时运行容器,并提取出需要的 etc 以及 root 配置文件:
docker run --rm --name tmp comfyui:latest sleep 15s &
docker cp tmp:/etc ./etc
docker cp tmp:/root ./root
接着启动容器
docker compose up -d
2.3. 完善 ComfyUI 容器
进入容器,安装 ComfyUI 以及常用节点:
docker exec -it comfyui /bin/bash
修改 ssh 远程连接的密码,允许使用 ssh 接入容器(可选):
passwd root
systemctl restart sshd
安装要用的套件,包括 zsh、 oh-my-zsh、uv等:
# 安装常用工具
apt update && apt install -y zsh
apt install -y vim wget git curl htop gcc make openssl p7zip-full unzip
# 安装 oh-my-zsh
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
# 安装 uv
curl -LsSf https://astral.sh/uv/install.sh | sh
因为 root 目录已经挂载到主机,所以以上配置会被保存下来,即使容器重建也不会丢失。
接着来到 ComfyUI 目录,安装 ComfyUI 需要的运行环境以及常用节点:
cd /comfy-ui
uv venv .venv
uv pip install -r requirements.txt
试运行:
python main.py
2.4. 备份
对于服务器而言,结合 ZFS 对 ComfyUI 目录进行快照是一个不错的选择,另外也可以使用下面的脚本进行定期备份。
备份只需备份两处位置:
- 容器环境:/docker/comfyui
- 代码环境:/mnt/sda/comfyui
为了方便,用以下脚本结合 crontab 实现自动定期备份,方便还原:
#!/usr/bin/env bash
# backup.sh - 稳健型目录备份脚本(Debian 12)
# 用法:./backup.sh <源目录> <备份前缀路径> <保留备份个数>
set -Euo pipefail
IFS=$' \t\n'
err() { echo "[ERROR] $*" >&2; }
warn() { echo "[WARN ] $*" >&2; }
info() { echo "[INFO ] $*"; }
need_cmd() { command -v "$1" >/dev/null 2>&1 || { err "需要命令:$1"; exit 127; }; }
need_cmd tar; need_cmd gzip; need_cmd date; need_cmd du; need_cmd find; need_cmd sort; need_cmd awk
need_cmd mktemp; need_cmd hostname; need_cmd curl; need_cmd stat
# ===== 参数 =====
if [[ $# -ne 3 ]]; then
err "用法:$0 <源目录> <备份前缀路径> <保留备份个数>"
exit 2
fi
SRC_DIR="$1"
DST_PREFIX="$2"
KEEP="$3"
[[ -d "$SRC_DIR" ]] || { err "源目录不存在:$SRC_DIR"; exit 2; }
DST_DIR="$(dirname "$DST_PREFIX")"
FILE_PREFIX="$(basename "$DST_PREFIX")"
mkdir -p "$DST_DIR"
if ! [[ "$KEEP" =~ ^[0-9]+$ ]] || [[ "$KEEP" -lt 1 ]]; then
err "保留备份个数必须为 >=1 的整数:$KEEP"
exit 2
fi
HOST="$(hostname -s || echo unknown-host)"
DATE_STR="$(date +%F)"
DATE_TAG="$(date +%Y%m%d)"
START_ISO="$(date -Is)"
SECONDS=0
SRC_BASENAME="$(basename "$SRC_DIR")"
# ===== 文件名规则(按日期命名)=====
ARCHIVE_NAME="${FILE_PREFIX}-${DATE_TAG}.tar.gz"
ARCHIVE_PATH="$DST_DIR/$ARCHIVE_NAME"
# ===== 信息(非致命)=====
SRC_SIZE_HUMAN="$(du -sh --apparent-size "$SRC_DIR" 2>/dev/null | awk '{print $1}')"
FILE_COUNT="$(find "$SRC_DIR" -type f 2>/dev/null | wc -l | awk '{print $1}')"
# ===== 流式压缩(不写 /tmp)=====
info "开始打包:$SRC_DIR -> $ARCHIVE_PATH"
PARENT_DIR="$(dirname "$SRC_DIR")"
BASE_DIR="$(basename "$SRC_DIR")"
pushd "$PARENT_DIR" >/dev/null
tar --xattrs --acls --one-file-system --numeric-owner \
--warning=no-file-changed --ignore-failed-read \
-cf - "$BASE_DIR" \
| gzip -9n > "$ARCHIVE_PATH"
# 立即捕获 PIPESTATUS
declare -a PIPE_STATUS=( "${PIPESTATUS[@]}" )
TAR_RC=${PIPE_STATUS[0]:-1}
GZIP_RC=${PIPE_STATUS[1]:-1}
popd >/dev/null
if [[ $TAR_RC -ne 0 || $GZIP_RC -ne 0 ]]; then
warn "tar/gzip 流式压缩失败:tar=$TAR_RC gzip=$GZIP_RC"
rm -f "$ARCHIVE_PATH"
fi
END_ISO="$(date -Is)"
ARCHIVE_SIZE_HUMAN="$(du -h "$ARCHIVE_PATH" | awk '{print $1}')"
# ===== 轮转 =====
info "执行备份轮转(保留 $KEEP 个)..."
readarray -d '' FILES < <(
find "$DST_DIR" -maxdepth 1 -type f \
-name "${FILE_PREFIX}-*.tar.gz" \
-print0 2>/dev/null
)
TOTAL="${#FILES[@]}"
if (( TOTAL == 0 )); then
info "未发现任何匹配备份。"
else
TMP_LIST="$(mktemp)"
for f in "${FILES[@]}"; do
printf "%s\t%s\n" "$(stat -c %Y "$f" 2>/dev/null || echo 0)" "$f" >> "$TMP_LIST"
done
mapfile -t SORTED < <(sort -nr -k1,1 "$TMP_LIST" | awk -F'\t' '{print $2}')
rm -f "$TMP_LIST"
info "当前匹配到 $TOTAL 个备份:"
for f in "${SORTED[@]}"; do
ts="$(stat -c %y "$f" 2>/dev/null | awk '{print $1" "$2}')"
printf " - %s %s\n" "$ts" "$f"
done
if (( TOTAL > KEEP )); then
for (( i=KEEP; i<TOTAL; i++ )); do
f="${SORTED[$i]}"
info "删除旧备份:$f"
rm -f -- "$f" || warn "删除失败:$f"
done
else
info "无需删除:总数 $TOTAL <= 保留数 $KEEP。"
fi
fi
info "备份完成。"