#!/usr/bin/env bash
# MCP ComfyUI Flux - Automated Installation Script (optimized)
# - Detects docker compose plugin or legacy docker-compose
# - Supports non-interactive mode (--yes / --non-interactive)
# - OS-aware Docker daemon guidance (Linux/macOS/WSL)
# - Safer version checks (sort -V), flock-based lock
# - Cached GPU detection; optional --cpu-only
# - Better health waits and diagnostics
# - Log rotation; color only when TTY / NO_COLOR not set
set -Eeuo pipefail
shopt -s inherit_errexit 2>/dev/null || true
umask 022
# --- Constants & Paths --------------------------------------------------------
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_ROOT="${SCRIPT_DIR}"
readonly LOCK_FILE="${PROJECT_ROOT}/.install.lock"
readonly LOG_FILE="${PROJECT_ROOT}/install.log"
readonly MIN_DOCKER_VERSION="20.10"
readonly MIN_COMPOSE_VERSION_LEGACY="1.29"
readonly MIN_COMPOSE_VERSION_PLUGIN="2.0"
readonly MIN_RAM_GB=16
readonly MIN_DISK_GB=50
readonly DEFAULT_PORT=8188
# Allow override without touching the readonly default
PORT="${PORT:-$DEFAULT_PORT}"
# --- Colors (TTY aware) -------------------------------------------------------
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
else
RED=""; GREEN=""; YELLOW=""; BLUE=""; NC=""
fi
# --- Globals modified by CLI --------------------------------------------------
ASSUME_YES="${ASSUME_YES:-0}" # set by --yes or --non-interactive
FORCE_CPU="${FORCE_CPU:-0}" # set by --cpu-only
DEBUG="${DEBUG:-0}"
SETUP_CLAUDE_CODE="${SETUP_CLAUDE_CODE:-true}"
PROJECT_NAME="${PROJECT_NAME:-mcp-comfyui-flux}"
MODELS_MODE="${MODELS_MODE:-auto}" # auto|minimal|all|none
# Compose command holder (array to preserve spaces)
COMPOSE=()
COMPOSE_VER=""
# Cached GPU probe
GPU_PRESENT="unknown" # values: unknown|yes|no
GPU_VRAM_GB="0"
DOCKER_GPU_OK="unknown" # yes|no|unknown
# --- Logging ------------------------------------------------------------------
rotate_log_if_large() {
if [[ -f "${LOG_FILE}" ]]; then
local size
size=$(wc -c < "${LOG_FILE}" || echo 0)
if (( size > 1048576 )); then
mv -f "${LOG_FILE}" "${LOG_FILE}.$(date +%Y%m%d_%H%M%S)"
fi
fi
}
log() {
local level="$1"; shift
local msg="$*"
local ts
ts=$(date '+%Y-%m-%d %H:%M:%S')
printf '[%s] [%s] %s\n' "$ts" "$level" "$msg" >> "${LOG_FILE}"
case "$level" in
ERROR) echo -e "${RED}[ERROR]${NC} $msg" >&2 ;;
WARN) echo -e "${YELLOW}[WARN]${NC} $msg" ;;
INFO) echo -e "${GREEN}[INFO]${NC} $msg" ;;
DEBUG) [[ "$DEBUG" == "1" ]] && echo -e "${BLUE}[DEBUG]${NC} $msg" ;;
esac
}
# --- Cleanup & error trap -----------------------------------------------------
cleanup() {
local exit_code=$?
# release flock FD 9 automatically on exit
rm -f "${LOCK_FILE}.hint" 2>/dev/null || true
if (( exit_code != 0 )); then
echo -e "${RED}Installation failed with exit code ${exit_code}${NC}" | tee -a "${LOG_FILE}" >&2
echo -e "${YELLOW}Check ${LOG_FILE} for details${NC}" >&2
fi
exit $exit_code
}
trap cleanup EXIT INT TERM
trap 'log ERROR "Command failed: ${BASH_COMMAND}"' ERR
# --- Utils --------------------------------------------------------------------
confirm() {
local prompt="${1:-Continue?}"
if [[ "${ASSUME_YES}" == "1" ]]; then
log DEBUG "Auto-confirmed: $prompt"
return 0
fi
read -rp "$prompt (y/N): " ans
[[ "$ans" =~ ^[Yy]$ ]]
}
ver_ge() {
# usage: ver_ge CURRENT MIN_REQUIRED
local cur="$1" req="$2"
[[ "$(printf '%s\n' "$req" "$cur" | sort -V | head -n1)" == "$req" ]]
}
require_cmd() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
log ERROR "$cmd is not installed"
return 1
fi
}
os_name() {
uname -s 2>/dev/null || echo "Unknown"
}
# Compose service running helper (avoids container-name assumptions)
compose_service_running() {
local svc="$1"
# Plugin (v2) supports --filter; legacy v1 does not.
if "${COMPOSE[@]}" ps --help 2>&1 | grep -q -- '--filter'; then
"${COMPOSE[@]}" -p "${PROJECT_NAME}" ps --services --filter "status=running" \
| grep -qx "$svc"
else
# Fallback: parse running containers and extract service name
"${COMPOSE[@]}" -p "${PROJECT_NAME}" ps \
| awk 'NR>2 && $0 ~ /Up/ {print $1}' \
| sed -E 's/^.+_([a-zA-Z0-9.-]+)_[0-9]+$/\1/' \
| grep -qx "$svc"
fi
}
# Port checks (cross-platform best-effort)
port_in_use() {
local p="$1"
if command -v ss >/dev/null 2>&1; then
ss -lnt "( sport = :$p )" 2>/dev/null | grep -q LISTEN
elif command -v lsof >/dev/null 2>&1; then
lsof -iTCP:"$p" -sTCP:LISTEN -nP >/dev/null 2>&1
else
netstat -an 2>/dev/null | grep -Eq "[\.:]$p[[:space:]].*LISTEN"
fi
}
check_port_free() {
if port_in_use "$PORT"; then
log ERROR "Port $PORT appears to be in use."
confirm "Continue anyway (Compose may remap)?" || exit 1
fi
}
# --- Lock with flock (prevents races) ----------------------------------------
acquire_lock() {
# create a human-readable hint for manual recovery
echo "If stuck, remove this file and re-run: ${LOCK_FILE}" > "${LOCK_FILE}.hint"
exec 9>"${LOCK_FILE}" || { log ERROR "Cannot open lock file"; exit 1; }
if ! flock -n 9; then
log ERROR "Another installation is in progress (lock held at ${LOCK_FILE})"
exit 1
fi
}
# --- Privileges ---------------------------------------------------------------
check_privileges() {
if [[ "${EUID}" -eq 0 ]]; then
log WARN "Running as root is not recommended."
confirm "Continue anyway?" || exit 1
fi
}
# --- Compose detection (plugin vs legacy) ------------------------------------
detect_compose() {
if docker compose version >/dev/null 2>&1; then
COMPOSE=(docker compose)
COMPOSE_VER="$(docker compose version --short 2>/dev/null || docker compose version | awk '{print $NF}')"
COMPOSE_VER="${COMPOSE_VER#v}"
if ! ver_ge "$COMPOSE_VER" "$MIN_COMPOSE_VERSION_PLUGIN"; then
log WARN "docker compose version $COMPOSE_VER < $MIN_COMPOSE_VERSION_PLUGIN (plugin)"
fi
log DEBUG "Using docker compose (plugin) v${COMPOSE_VER}"
return 0
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE=(docker-compose)
COMPOSE_VER="$(docker-compose --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo 0)"
if ! ver_ge "$COMPOSE_VER" "$MIN_COMPOSE_VERSION_LEGACY"; then
log ERROR "docker-compose $COMPOSE_VER < $MIN_COMPOSE_VERSION_LEGACY"
return 1
fi
log DEBUG "Using legacy docker-compose v${COMPOSE_VER}"
return 0
else
log ERROR "Neither 'docker compose' nor 'docker-compose' found"
log INFO "Install Docker Desktop/Engine with Compose plugin: https://docs.docker.com/get-docker/"
return 1
fi
}
# --- Docker daemon ------------------------------------------------------------
check_docker_running() {
local attempts=3
local i=1
while (( i <= attempts )); do
if docker info >/dev/null 2>&1; then
log DEBUG "Docker daemon is running"
return 0
fi
log WARN "Docker daemon not responding (attempt $i/$attempts)"
case "$(os_name)" in
Linux)
if command -v systemctl >/dev/null 2>&1; then
log INFO "Trying to start Docker via systemd..."
sudo systemctl start docker || true
sleep 2
else
log INFO "On Linux without systemd: start the Docker daemon manually."
fi
;;
Darwin)
log INFO "On macOS, open Docker Desktop and wait for it to start."
;;
MINGW*|MSYS*|CYGWIN*|Windows_NT)
log INFO "On Windows, start Docker Desktop."
;;
esac
((i++))
done
log ERROR "Docker daemon is not running. Start Docker and retry."
return 1
}
# --- System resources ---------------------------------------------------------
check_system_resources() {
log INFO "Checking system resources..."
# RAM
if [[ -f /proc/meminfo ]]; then
local total_ram_kb total_ram_gb
total_ram_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
total_ram_gb=$(( total_ram_kb / 1024 / 1024 ))
if (( total_ram_gb < MIN_RAM_GB )); then
log WARN "System has ${total_ram_gb}GB RAM; recommended >= ${MIN_RAM_GB}GB"
confirm "Continue with limited RAM?" || return 1
else
log DEBUG "RAM check passed: ${total_ram_gb}GB"
fi
fi
# Disk
local available_space_gb
# POSIX/BSD/GNU portable: report in KiB, convert to GiB
available_space_gb=$(df -Pk "${PROJECT_ROOT}" | awk 'NR==2 {print int($4/1024/1024)}')
if (( available_space_gb < MIN_DISK_GB )); then
log WARN "Only ${available_space_gb}GB free; recommended >= ${MIN_DISK_GB}GB"
confirm "Continue with limited disk space?" || return 1
else
log DEBUG "Disk space check passed: ${available_space_gb}GB available"
fi
return 0
}
# --- GPU detection (cached) ---------------------------------------------------
detect_gpu() {
if [[ "${FORCE_CPU}" == "1" ]]; then
GPU_PRESENT="no"; GPU_VRAM_GB="0"; DOCKER_GPU_OK="no"
log INFO "CPU-only mode forced by flag."
return 1
fi
if [[ "${GPU_PRESENT}" != "unknown" ]]; then
[[ "${GPU_PRESENT}" == "yes" ]] && return 0 || return 1
fi
if ! command -v nvidia-smi >/dev/null 2>&1; then
log WARN "nvidia-smi not found — assuming no NVIDIA GPU."
GPU_PRESENT="no"; DOCKER_GPU_OK="no"; return 1
fi
if ! nvidia-smi >/dev/null 2>&1; then
log WARN "NVIDIA driver installed but GPU not accessible."
GPU_PRESENT="no"; DOCKER_GPU_OK="no"; return 1
fi
# Get GPU information
local gpu_info
gpu_info=$(nvidia-smi --query-gpu=name,memory.total --format=csv,noheader 2>/dev/null || true)
if [[ -n "${gpu_info}" ]]; then
log INFO "Detected GPU: ${gpu_info}"
fi
local vram_mb
vram_mb=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null | head -1 || echo 0)
GPU_VRAM_GB=$(( vram_mb / 1024 ))
GPU_PRESENT="yes"
if (( GPU_VRAM_GB < 12 )); then
log WARN "GPU VRAM ${GPU_VRAM_GB}GB < 12GB (Flux runs best with 12GB+)."
confirm "Continue with limited VRAM?" || { GPU_PRESENT="no"; return 1; }
fi
# Verify Docker GPU support *without* pulling heavy images:
# Prefer checking for NVIDIA runtime presence first.
if docker info --format '{{json .Runtimes}}' 2>/dev/null | grep -qi nvidia; then
DOCKER_GPU_OK="yes"
log INFO "NVIDIA runtime detected in Docker."
return 0
fi
if [[ "${DOCKER_GPU_OK}" != "yes" ]]; then
log WARN "Docker GPU support not available."
log INFO "Enable with NVIDIA Container Toolkit: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html"
if confirm "Continue without GPU acceleration?"; then
# Ensure subsequent checks treat this run as CPU-only
GPU_PRESENT="no"
DOCKER_GPU_OK="no"
return 1
else
return 1
fi
fi
return 1
}
# --- Environment setup --------------------------------------------------------
setup_env() {
log INFO "Setting up environment configuration..."
local env_file="${PROJECT_ROOT}/.env"
local env_example="${PROJECT_ROOT}/.env.example"
[[ -f "${env_example}" ]] || { log ERROR ".env.example not found"; return 1; }
if [[ -f "${env_file}" ]]; then
local backup="${env_file}.backup.$(date +%Y%m%d_%H%M%S)"
cp -f "${env_file}" "${backup}"
log INFO "Backed up existing .env → ${backup}"
confirm "Use existing .env?" && return 0
fi
cp -f "${env_example}" "${env_file}"
log DEBUG "Created .env from template."
# HF token
echo
echo "Flux.1-dev requires Hugging Face authentication. You can skip (Flux.1-schnell works without)."
if confirm "Configure Hugging Face token now?"; then
read -rsp "Enter your Hugging Face token: " hf_token; echo
if [[ -n "${hf_token}" ]]; then
sed -i.bak "s|^HF_TOKEN=.*|HF_TOKEN=${hf_token}|" "${env_file}" && rm -f "${env_file}.bak"
log INFO "Hugging Face token configured."
else
log WARN "Empty token provided; skipping HF configuration."
fi
fi
# GPU/CPU mode
if detect_gpu; then
log INFO "GPU detected (${GPU_VRAM_GB}GB); enabling GPU mode."
# Leave CUDA_VISIBLE_DEVICES as default (or unset) for GPU containers
else
sed -i.bak "s|^CUDA_VISIBLE_DEVICES=.*|CUDA_VISIBLE_DEVICES=-1|" "${env_file}" && rm -f "${env_file}.bak"
log INFO "Configured CPU-only mode in .env"
fi
}
# --- Models -------------------------------------------------------------------
download_models() {
log INFO "Model download setup (mode: ${MODELS_MODE})..."
local models_script="${PROJECT_ROOT}/scripts/download-models.sh"
local models_dir="${PROJECT_ROOT}/models"
[[ -f "${models_script}" ]] || { log ERROR "Missing ${models_script}"; return 1; }
chmod +x "${models_script}"
# Check for existing FLUX models in ./models directory
local flux_dev_found=false
local flux_schnell_found=false
local sd15_found=false
if [[ -d "${models_dir}/FLUX.1-dev" ]]; then
if [[ -f "${models_dir}/FLUX.1-dev/flux1-dev.safetensors" ]] || \
[[ -f "${models_dir}/FLUX.1-dev/transformer/diffusion_pytorch_model.safetensors" ]]; then
flux_dev_found=true
log INFO "Found existing FLUX.1-dev model in ${models_dir}/FLUX.1-dev"
fi
fi
if [[ -d "${models_dir}/FLUX.1-schnell" ]]; then
if [[ -f "${models_dir}/FLUX.1-schnell/flux1-schnell.safetensors" ]] || \
[[ -f "${models_dir}/FLUX.1-schnell/transformer/diffusion_pytorch_model.safetensors" ]]; then
flux_schnell_found=true
log INFO "Found existing FLUX.1-schnell model in ${models_dir}/FLUX.1-schnell"
fi
fi
# Check for SD1.5 models
if [[ -d "${models_dir}/sd1.5" ]]; then
if ls "${models_dir}/sd1.5"/*.safetensors >/dev/null 2>&1; then
sd15_found=true
log INFO "Found existing SD1.5 models in ${models_dir}/sd1.5"
fi
fi
# Create model directories structure
mkdir -p "${models_dir}"/{unet,clip,vae,checkpoints,loras,embeddings}
# Link or copy existing models to ComfyUI expected locations
if [[ "${flux_dev_found}" == "true" ]] || [[ "${flux_schnell_found}" == "true" ]] || [[ "${sd15_found}" == "true" ]]; then
log INFO "Setting up existing models for ComfyUI..."
# Link FLUX models to unet directory
if [[ "${flux_dev_found}" == "true" ]]; then
if [[ -f "${models_dir}/FLUX.1-dev/flux1-dev.safetensors" ]]; then
# Create relative symlink from unet to FLUX.1-dev
(cd "${models_dir}/unet" && ln -sf "../FLUX.1-dev/flux1-dev.safetensors" "flux1-dev.safetensors" 2>/dev/null) || \
cp "${models_dir}/FLUX.1-dev/flux1-dev.safetensors" "${models_dir}/unet/flux1-dev.safetensors"
log INFO "Linked FLUX.1-dev model to unet directory"
fi
# Link VAE if exists
if [[ -f "${models_dir}/FLUX.1-dev/ae.safetensors" ]]; then
(cd "${models_dir}/vae" && ln -sf "../FLUX.1-dev/ae.safetensors" "ae.safetensors" 2>/dev/null) || \
cp "${models_dir}/FLUX.1-dev/ae.safetensors" "${models_dir}/vae/ae.safetensors"
log INFO "Linked FLUX.1-dev VAE to vae directory"
fi
# Link CLIP models if exist
if [[ -f "${models_dir}/FLUX.1-dev/text_encoder/model.safetensors" ]]; then
(cd "${models_dir}/clip" && ln -sf "../FLUX.1-dev/text_encoder/model.safetensors" "clip_l.safetensors" 2>/dev/null) || \
cp "${models_dir}/FLUX.1-dev/text_encoder/model.safetensors" "${models_dir}/clip/clip_l.safetensors"
log INFO "Linked CLIP-L model to clip directory"
fi
if [[ -f "${models_dir}/FLUX.1-dev/text_encoder_2/model-00001-of-00002.safetensors" ]]; then
# T5 model is split into multiple files, link the directory instead
(cd "${models_dir}/clip" && ln -sf "../FLUX.1-dev/text_encoder_2" "t5xxl" 2>/dev/null) || \
cp -r "${models_dir}/FLUX.1-dev/text_encoder_2" "${models_dir}/clip/t5xxl"
log INFO "Linked T5-XXL model to clip directory"
fi
fi
if [[ "${flux_schnell_found}" == "true" ]]; then
if [[ -f "${models_dir}/FLUX.1-schnell/flux1-schnell.safetensors" ]]; then
(cd "${models_dir}/unet" && ln -sf "../FLUX.1-schnell/flux1-schnell.safetensors" "flux1-schnell.safetensors" 2>/dev/null) || \
cp "${models_dir}/FLUX.1-schnell/flux1-schnell.safetensors" "${models_dir}/unet/flux1-schnell.safetensors"
log INFO "Linked FLUX.1-schnell model to unet directory"
fi
# Link VAE if exists
if [[ -f "${models_dir}/FLUX.1-schnell/ae.safetensors" ]]; then
(cd "${models_dir}/vae" && ln -sf "../FLUX.1-schnell/ae.safetensors" "ae-schnell.safetensors" 2>/dev/null) || \
cp "${models_dir}/FLUX.1-schnell/ae.safetensors" "${models_dir}/vae/ae-schnell.safetensors"
log INFO "Linked FLUX.1-schnell VAE to vae directory"
fi
# Link CLIP models if exist (Schnell uses same CLIP models as dev)
if [[ ! -f "${models_dir}/clip/clip_l.safetensors" ]] && [[ -f "${models_dir}/FLUX.1-schnell/text_encoder/model.safetensors" ]]; then
(cd "${models_dir}/clip" && ln -sf "../FLUX.1-schnell/text_encoder/model.safetensors" "clip_l.safetensors" 2>/dev/null) || \
cp "${models_dir}/FLUX.1-schnell/text_encoder/model.safetensors" "${models_dir}/clip/clip_l.safetensors"
log INFO "Linked CLIP-L model from schnell to clip directory"
fi
if [[ ! -d "${models_dir}/clip/t5xxl" ]] && [[ -f "${models_dir}/FLUX.1-schnell/text_encoder_2/model-00001-of-00002.safetensors" ]]; then
(cd "${models_dir}/clip" && ln -sf "../FLUX.1-schnell/text_encoder_2" "t5xxl" 2>/dev/null) || \
cp -r "${models_dir}/FLUX.1-schnell/text_encoder_2" "${models_dir}/clip/t5xxl"
log INFO "Linked T5-XXL model from schnell to clip directory"
fi
fi
# Link SD1.5 models to checkpoints directory
if [[ "${sd15_found}" == "true" ]]; then
for sd_model in "${models_dir}"/sd1.5/*.safetensors; do
if [[ -f "${sd_model}" ]]; then
local model_name=$(basename "${sd_model}")
# Keep exact filename - ComfyUI workflows expect specific names
# Create relative symlink from checkpoints to sd1.5
(cd "${models_dir}/checkpoints" && ln -sf "../sd1.5/${model_name}" "${model_name}" 2>/dev/null) || \
cp "${sd_model}" "${models_dir}/checkpoints/${model_name}"
log INFO "Linked SD1.5 model: ${model_name}"
fi
done
# Check for SD VAE files if any
if [[ -f "${models_dir}/sd1.5/vae-ft-mse-840000-ema-pruned.safetensors" ]]; then
(cd "${models_dir}/vae" && ln -sf "../sd1.5/vae-ft-mse-840000-ema-pruned.safetensors" "vae-ft-mse-840000.safetensors" 2>/dev/null) || \
cp "${models_dir}/sd1.5/vae-ft-mse-840000-ema-pruned.safetensors" "${models_dir}/vae/vae-ft-mse-840000.safetensors"
log INFO "Linked SD1.5 VAE to vae directory"
fi
fi
# Check if we have at least one model ready
if [[ "${flux_dev_found}" == "true" ]] || [[ "${flux_schnell_found}" == "true" ]] || [[ "${sd15_found}" == "true" ]]; then
log INFO "Existing models detected and configured."
# Ask if user wants to download additional models
if confirm "Models already exist. Skip downloading additional models?"; then
log INFO "Using existing models, skipping downloads."
return 0
fi
fi
fi
# src .env so HF_TOKEN is available
if [[ -f "${PROJECT_ROOT}/.env" ]]; then
set -a; # shellcheck disable=SC1091
source "${PROJECT_ROOT}/.env";
set +a;
fi
case "${MODELS_MODE}" in
none) log INFO "Skipping model downloads (--models=none)"; return 0 ;;
minimal|all|auto) : ;;
*) log WARN "Unknown MODELS_MODE=${MODELS_MODE}; defaulting to auto";;
esac
if [[ -d "${models_dir}/unet" ]] && [[ -n "$(ls -A "${models_dir}/unet" 2>/dev/null || true)" ]]; then
log INFO "Some models already present in unet directory."
confirm "Download additional models?" || return 0
fi
if ! "${models_script}" "${MODELS_MODE}"; then
log WARN "Model download encountered issues."
confirm "Continue without completing model downloads?" || return 1
fi
}
# --- Build --------------------------------------------------------------------
build_docker() {
log INFO "Building Docker images..."
[[ -f "${PROJECT_ROOT}/docker-compose.yml" ]] || { log ERROR "docker-compose.yml not found"; return 1; }
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
# Clean older containers (optional)
if "${COMPOSE[@]}" -p "${PROJECT_NAME}" ps -a 2>/dev/null | grep -Eq "(comfyui|mcp-comfyui)"; then
log WARN "Existing project containers found."
if confirm "Remove existing containers?"; then
"${COMPOSE[@]}" -p "${PROJECT_NAME}" down --remove-orphans || true
fi
fi
# Determine build args based on GPU availability
if detect_gpu; then
log INFO "Building with GPU support (PyTorch 2.5.1)..."
else
log INFO "Building for CPU-only mode..."
fi
log INFO "Building optimized containers with BuildKit..."
log INFO "This may take several minutes on first build (using cache for subsequent builds)..."
# Use BuildKit for optimized builds
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
export BUILDKIT_PROGRESS=plain
if ! "${COMPOSE[@]}" -p "${PROJECT_NAME}" build 2>&1 | tee -a "${LOG_FILE}"; then
log ERROR "Docker build failed."
return 1
fi
log INFO "Docker images built successfully (optimized with PyTorch 2.5.1)."
}
# --- Start & health -----------------------------------------------------------
start_services() {
# Check port before bringing services up
check_port_free
log INFO "Starting services..."
if ! "${COMPOSE[@]}" -p "${PROJECT_NAME}" up -d 2>&1 | tee -a "${LOG_FILE}"; then
log ERROR "Failed to start services."
return 1
fi
log INFO "Waiting for ComfyUI to become ready on http://localhost:${PORT} ..."
require_cmd curl || { log ERROR "curl is required for health checks."; return 1; }
local max_wait=90 elapsed=0 step=3
while (( elapsed < max_wait )); do
if curl -fsS --max-time 5 "http://localhost:${PORT}/system_stats" >/dev/null 2>&1; then
log INFO "ComfyUI is ready!"
break
fi
printf '.'
sleep "${step}"
elapsed=$((elapsed + step))
done
echo
if (( elapsed >= max_wait )); then
log ERROR "Services did not become healthy within ${max_wait}s."
log INFO "Inspect logs via: ${COMPOSE[*]} -p ${PROJECT_NAME} logs --tail=200"
return 1
fi
# Add a small delay for container health checks to register
sleep 2
if compose_service_running "mcp-server"; then
log INFO "MCP server service is running."
else
log WARN "MCP server service not found; checking alternate names..."
# Check if container exists with project prefix
if docker ps --format '{{.Names}}' | grep -q "mcp-.*-server"; then
log INFO "MCP server container found with project prefix."
else
log WARN "MCP server container not found; verify your compose file names/services."
fi
fi
}
# --- MCP client config snippet ------------------------------------------------
show_mcp_config() {
cat << EOF
${BLUE}════════════════════════════════════════════════════════════${NC}
${GREEN}Add to your MCP client configuration:${NC}
For Claude Desktop or other MCP clients:
${YELLOW}{
"mcpServers": {
"comfyui-flux": {
"command": "docker",
"args": [
"compose", "-p", "${PROJECT_NAME}",
"exec", "-T", "mcp-server",
"node", "/app/src/index.js"
]
}
}
}${NC}
Alternative (direct connection):
${YELLOW}{
"mcpServers": {
"comfyui-flux": {
"command": "node",
"args": ["${PROJECT_ROOT}/src/index.js"],
"env": {
"COMFYUI_HOST": "localhost",
"COMFYUI_PORT": "${PORT}"
}
}
}
}${NC}
${BLUE}════════════════════════════════════════════════════════════${NC}
EOF
}
# --- Status -------------------------------------------------------------------
show_status() {
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Installation Complete! 🎉 ║${NC}"
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
echo
echo "Service Status:"
"${COMPOSE[@]}" -p "${PROJECT_NAME}" ps
echo
echo -e "${GREEN}Access Points:${NC}"
echo " • ComfyUI: http://localhost:${PORT}"
echo " • MCP Server container: ${PROJECT_NAME}-mcp-server-1"
echo
echo -e "${GREEN}Useful Commands:${NC}"
echo " • View logs: ${COMPOSE[*]} -p ${PROJECT_NAME} logs -f"
echo " • Stop services: ${COMPOSE[*]} -p ${PROJECT_NAME} down"
echo " • Restart: ${COMPOSE[*]} -p ${PROJECT_NAME} restart"
echo " • Update: git pull && ${COMPOSE[*]} -p ${PROJECT_NAME} build"
if [[ "${GPU_PRESENT}" == "yes" ]]; then
echo " • Check GPU: ${COMPOSE[*]} -p ${PROJECT_NAME} exec comfyui nvidia-smi"
fi
echo
echo -e "${GREEN}Log file: ${LOG_FILE}${NC}"
}
# --- Main ---------------------------------------------------------------------
main() {
rotate_log_if_large
echo "Installation started at $(date)" > "${LOG_FILE}"
echo -e "${BLUE}╔══════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ MCP ComfyUI Flux - Installation Script ║${NC}"
echo -e "${BLUE}╚══════════════════════════════════════════════════════════╝${NC}"
echo
check_privileges
acquire_lock
log INFO "Checking prerequisites..."
require_cmd docker || { log ERROR "Install Docker first."; exit 1; }
detect_compose || exit 1
require_cmd curl || { log ERROR "Install curl for health checks."; exit 1; }
# Docker version
local docker_ver
docker_ver="$(docker --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo 0)"
ver_ge "$docker_ver" "$MIN_DOCKER_VERSION" || { log ERROR "Docker $docker_ver < $MIN_DOCKER_VERSION"; exit 1; }
check_docker_running || exit 1
check_system_resources || exit 1
log INFO "Prerequisites satisfied."
setup_env || exit 1
download_models || log WARN "Continuing without completing model downloads."
build_docker || exit 1
start_services || exit 1
show_mcp_config
show_status
if [[ "${SETUP_CLAUDE_CODE}" == "true" ]]; then
if [[ -x "${PROJECT_ROOT}/scripts/setup-claude-code.sh" ]]; then
log INFO "Setting up Claude Code..."
if "${PROJECT_ROOT}/scripts/setup-claude-code.sh"; then
log INFO "Claude Code setup completed."
else
log WARN "Claude Code setup encountered issues."
fi
else
log DEBUG "Claude Code setup script not present; skipping."
fi
fi
log INFO "Installation completed successfully."
}
# --- CLI ----------------------------------------------------------------------
usage() {
cat <<EOF
Usage: $0 [OPTIONS]
Options:
--yes, --non-interactive Auto-confirm all prompts (CI-friendly).
--debug Enable verbose debug logs.
--cpu-only Force CPU-only mode (skip GPU checks).
--models {auto|minimal|all|none}
Control model download behavior (default: auto).
--project-name NAME Set docker compose project name (default: ${PROJECT_NAME}).
--port N Override ComfyUI port (default: ${DEFAULT_PORT}).
--help, -h Show this help.
Examples:
$0 --yes --cpu-only --models=minimal
$0 --project-name myflux --debug
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--yes|--non-interactive) ASSUME_YES=1 ;;
--debug) DEBUG=1 ;;
--cpu-only) FORCE_CPU=1 ;;
--models)
shift
MODELS_MODE="${1:-auto}"
;;
--project-name)
shift
PROJECT_NAME="${1:-mcp-comfyui-flux}"
;;
--port)
shift
PORT="${1:-$DEFAULT_PORT}"
;;
--help|-h) usage; exit 0 ;;
*)
log ERROR "Unknown option: $1"
usage
exit 1
;;
esac
shift
done
main