comfyforge
v1.2.0
Published
Generate ComfyUI configs and bake them into deployable Docker images.
Maintainers
Readme
ComfyForge
CLI + web app to build a config.json for the ComfyUI Cloud Docker container and bake it into a Docker image ready to cold-start in seconds on RunPod, Vast.ai, Modal, and other GPU clouds.
Two ways to use it:
| Command | What it does |
|---|---|
| comfyforge init (default) | Interactive wizard — generates config.json from a workflow + a selection of models and workflows. |
| comfyforge fetch <config> | Downloads every model + workflow from a config into a local directory using ComfyUI's layout. Reusable as a bake cache or as a sync target for a local ComfyUI install. |
| comfyforge cache status <dir> | Inspects a local assets cache: sizes by category, .partial files, optional coverage vs a config. |
| comfyforge bake <config> | Takes a config.json (file or URL) and runs docker buildx to create an image with everything pre-installed, with a deterministic tag derived from the config hash. Supports --assets-dir to reuse files from a previous fetch. |
There's also a web app (/web) — same UX, with a workflow dropzone, HuggingFace/Civitai pickers, a live config tree preview, and a "Bake tab" that generates a recipe zip (Dockerfile.bake + .env.example + README) so you can run docker buildx from your own terminal without installing anything.
Install
# Run directly with npx (no install required)
npx comfyforge --help
# Or install globally
npm install -g comfyforge
comfyforge --helpPrerequisite for bake: Docker with BuildKit (any recent Docker).
comfyforge init — generate the config
# Full wizard starting from a workflow
comfyforge init -w path/to/workflow.json
# Just extract nodes from the workflow, no wizard
comfyforge init -w workflow.json --no-interactive -o config.json
# Pure wizard (no workflow)
comfyforge initThe wizard:
- Shows the custom node repos extracted from the workflow for you to confirm/uncheck.
- Lets you add models by URL (HuggingFace, Civitai, direct link) with category and filename.
- Optionally adds workflows to be downloaded at boot.
Node mapping
- If the node has
properties.aux_id(formatowner/repo) → uses it directly. - Otherwise, matches by pattern in the
type(e.g.(rgthree)→rgthree/rgthree-comfy). - ComfyUI core nodes are ignored.
- To add new mappings: edit
src/nodeMap.ts.
Output
{
"nodes": [
"https://github.com/kijai/ComfyUI-KJNodes",
{ "url": "https://github.com/city96/ComfyUI-GGUF", "ref": "v0.4.0" }
],
"models": [
{ "category": "diffusion_models", "url": "https://...", "filename": "model.safetensors" }
],
"workflows": []
}Drop this into a Gist (or any public URL) and use it as CONFIG_URL at runtime — or pass it as input to bake.
comfyforge fetch — download assets locally
Downloads every models[] + workflows[] entry from a config into a local
directory laid out exactly like ComfyUI's /workspace. Two main uses:
- Bake cache — prime a folder once, then run
bake --assets-dir <folder>as many times as you want; assets already on disk are reused (no re-download inside the build). - Sync a local ComfyUI install — point
--outstraight at yourComfyUI/folder; models land inmodels/<category>/and workflows land inuser/default/workflows/where the UI picks them up automatically.
# First time: download everything
export HF_TOKEN=... # only if any model is gated
export CIVITAI_TOKEN=... # required if config references civitai.com
comfyforge fetch ./my-config.json --out ~/comfy-cache
# Re-run later — only missing files are fetched
comfyforge fetch ./my-config.json --out ~/comfy-cache
# Just check what's present vs missing
comfyforge fetch ./my-config.json --out ~/comfy-cache --status
# Plan only (no downloads)
comfyforge fetch ./my-config.json --out ~/comfy-cache --dry-run
# Parallel downloads (default 1). Good for many small workflows/VAEs/LoRAs.
comfyforge fetch ./my-config.json --out ~/comfy-cache --concurrency 4Layout produced
<out>/
models/
diffusion_models/<file> # from { category: "diffusion_models", ... }
vae/<file>
loras/<file>
...
user/
default/
workflows/<file> # from workflows[]Reuse rule
If a file already exists at the target path with the same filename and is
non-empty, the fetch skips it — no hash check, no size check. The same
rule applies inside bake --assets-dir, so the two commands stay consistent.
Flags
| Flag | What it does |
|---|---|
| -o, --out <path> | Required. Target directory (created if missing). |
| --hf-token <v> / --civitai-token <v> | Tokens; also accepts HF_TOKEN / CIVITAI_TOKEN from env. |
| -c, --concurrency <n> | Number of files to download in parallel (default 1). Larger values help when many small files dominate; large single files still bottleneck on the server. |
| --dry-run | List what would be downloaded without fetching. |
| --status | Print which items are present/missing in <out> and exit. |
| --no-interactive | No prompts (CI). |
URL handling
The fetcher auto-rewrites two common URL mistakes so downloads succeed:
huggingface.co/.../blob/<ref>/...→.../resolve/<ref>/...github.com/<user>/<repo>/blob/<ref>/<path>→raw.githubusercontent.com/<user>/<repo>/<ref>/<path>
For model URLs, the fetcher also checks the response Content-Type and
aborts with a clear error if the server returns HTML (typical sign of a
webpage URL, an expired link, or a missing auth token).
comfyforge cache status — inspect a local cache
# What's in this cache directory?
comfyforge cache status ~/comfy-cache
# Cross-reference against a config: which assets are present, which are missing?
comfyforge cache status ~/comfy-cache --config ./my-config.jsonOutput includes:
- File counts and total size per model category (
diffusion_models,vae,loras, etc.) - Workflows count + size
- A list of
.partialfiles (interrupted downloads occupying disk space) - With
--config: coverage percentage and the exact list of missing assets, matching whatbake --assets-dir --strictwould require.
comfyforge bake — bake the image
Takes the config.json, generates a Dockerfile.bake that inherits from the official base image (tcpassos/comfyui-cloud:latest), pre-downloads every node/model/workflow listed in the config inside the image, and runs docker buildx build --push (or --load with --no-push).
The tag is deterministic: cfg-<sha12>[-<name>]. Identical configs always produce the same tag → rebuilds are free.
Examples
# Bake + push to GHCR
comfyforge bake ./my-config.json \
--image ghcr.io/your-user/comfy-flux \
--name flux-dev
# Local bake (no push)
comfyforge bake ./my-config.json \
--image docker.io/your-user/comfy-flux \
--no-push
# Dry-run — only generates the Dockerfile.bake and prints the docker command, without running it
comfyforge bake ./my-config.json \
--image ghcr.io/your-user/comfy-flux --dry-run --keep
# Bake from a URL (Gist, raw GitHub, etc.)
comfyforge bake https://gist.githubusercontent.com/.../config.json \
--image ghcr.io/your-user/comfy-flux
# Reuse a local cache populated by `comfyforge fetch`
# (models/workflows already in the cache are NOT re-downloaded during the build)
comfyforge fetch ./my-config.json --out ~/comfy-cache
comfyforge bake ./my-config.json --image ghcr.io/your-user/comfy-flux \
--assets-dir ~/comfy-cache
# Strict mode — fail if anything is missing from the cache, no network fetches
comfyforge bake ./my-config.json --image ghcr.io/your-user/comfy-flux \
--assets-dir ~/comfy-cache --strict--keep preserves the temp directory with the generated recipe (useful for inspecting/customizing before running manually).
Flags
| Flag | What it does |
|---|---|
| -i, --image <repo> | Full repo without tag, e.g. ghcr.io/user/comfy-flux. |
| -r, --registry <ghcr\|dockerhub\|custom> + -u, --user + --repo | Alternative to --image. |
| -n, --name <name> | Friendly name (becomes the tag suffix and an image label). |
| -b, --base <image> | Base image (default tcpassos/comfyui-cloud:latest). |
| --no-push | Loads into local Docker instead of pushing. |
| --dry-run | Only generates files and prints the command, without calling docker. |
| --keep | Don't delete the recipe directory at the end. |
| --hf-token <v> / --civitai-token <v> | Tokens; also accepts HF_TOKEN / CIVITAI_TOKEN from env. |
| --no-interactive | No prompts (CI). |
| --assets-dir <path> | Reuse pre-downloaded models/workflows from a local cache (typically created by comfyforge fetch). Files already present in the cache are copied into the image — the build only fetches what's missing. |
| --strict | With --assets-dir, fail before invoking docker if any required asset is missing from the cache. No network downloads happen. Useful for air-gapped / reproducible builds. |
Secrets detection
When loading the config, bake detects:
- URLs in
huggingface.co/...→ warns that gated repos requireHF_TOKEN. - URLs in
civitai.com/...→ Civitai always requiresCIVITAI_TOKEN.
In TTY (default), it only asks for the relevant tokens. In CI mode, it prints !! warnings before the build.
Tokens are passed via BuildKit secrets (--mount=type=secret), never copied into image layers.
Registry authentication
The CLI does not run docker login automatically — it assumes you're already logged in:
# GHCR
echo $GHCR_PAT | docker login ghcr.io -u <user> --password-stdin
# Docker Hub
docker login docker.ioWeb app (/web)
cd web
npm install
npm run dev # http://localhost:5173Same feature set as the CLI:
- Workflow dropzone → node extraction.
- HuggingFace pickers (with auto-resolved filenames) and Civitai (with version selector).
- Live config tree + volume estimate.
- Bake tab: generates a "recipe zip" (Dockerfile.bake + bake.sh + .env.example + README) so you can run it locally without installing the CLI — or copies a one-liner
npx comfyforge bake ...command if you'd rather use the CLI.
Extra features that need the backend (/api) running:
- GitHub login — sign in to save private/public configs.
- My library — personal collection of saved configs, with edit (name, description, tags, visibility).
- Public gallery (
/gallery) — search/filter community-shared configs by tag. - Permalinks (
/u/:login/:slug) — every public/unlisted config gets a stable deploy-ready URL likehttps://api.comfyforge.app/c/u/<login>/<slug>.jsonthat you can plug directly intoCONFIG_URLon RunPod/Vast/Modal.
The web app and the CLI share the same deterministic hash algorithm (web/src/lib/configHash.ts and src/lib/configHash.ts) and the same Dockerfile templates, so the same config produces exactly the same tag through either path.
Backend (/api)
Optional Express + SQLite service that powers anonymous config sharing, GitHub-authenticated personal library, and the public gallery. The web app works without the backend (CLI-style features stay local); the backend only unlocks login/library/gallery.
Run locally
cd api
npm install
cp .env.example .env # then edit (see below)
npm run dev # http://localhost:3000 (tsx watch)You should see:
comfyforge-api listening on :3000
DATA_DIR = ./data
PUBLIC_BASE_URL = http://localhost:3000/c
auth = enabled.env for local dev
PORT=3000
DATA_DIR=./data
PUBLIC_BASE_URL=http://localhost:3000/c
ALLOWED_ORIGIN=http://localhost:5173
FRONTEND_URL=http://localhost:5173
PUBLIC_API_URL=http://localhost:3000
# GitHub OAuth — optional. Without these, /me returns null
# and library/gallery write endpoints return 401, but the API still runs.
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
# COOKIE_DOMAIN=.comfyforge.app # only in prod| Var | Purpose |
|---|---|
| DATA_DIR | Folder where SQLite files live (auth.sqlite, library.sqlite, resolve-cache.sqlite, plus *.json configs). Use ./data in dev, a mounted volume in prod. |
| PUBLIC_BASE_URL | Base URL printed back to clients when they upload an anonymous config. |
| ALLOWED_ORIGIN | Must match the frontend origin exactly for CORS + cookies to work. Use * only if you don't need credentials. |
| FRONTEND_URL / PUBLIC_API_URL | Used by the OAuth callback to know where to redirect and what redirect_uri to register with GitHub. |
| GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET | From a GitHub OAuth App. Callback URL must be ${PUBLIC_API_URL}/auth/github/callback. |
| ADMIN_TOKEN | Optional bearer token for /resolve/misses admin endpoint. |
Setting up GitHub OAuth (for local login)
- https://github.com/settings/developers → New OAuth App
- Homepage URL:
http://localhost:5173 - Authorization callback URL:
http://localhost:3000/auth/github/callback - Paste the Client ID + a generated Client Secret into
api/.env.
Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /me | optional | Current user or { user: null }. |
| GET | /auth/github | — | Starts GitHub OAuth (sets cf_sess httpOnly cookie on callback). |
| GET | /auth/github/callback | — | OAuth callback (do not call directly). |
| POST | /auth/logout | required | Clears the session. |
| GET | /library | required | List your saved configs. |
| POST | /library | required | Save a new config ({ name, description?, visibility, tags?, payload }). |
| GET | /library/:id | required (owner) | Get full config including payload. |
| PATCH | /library/:id | required (owner) | Update metadata (name, description, visibility, tags). |
| DELETE | /library/:id | required (owner) | Delete. |
| GET | /gallery?q=&tag=&sort=recent\|popular&limit=&offset= | — | Public gallery listing. |
| GET | /c/u/:login/:slug.json | — | Raw config payload (deploy URL — plug into CONFIG_URL). Increments view count. |
| GET | /c/u/:login/:slug | — | Public metadata for the permalink page. |
| POST | /configs | — | Anonymous one-shot upload (legacy flow, used by the CLI/web for ephemeral sharing). |
| GET | /c/:filename | — | Fetch an anonymous config (legacy). |
| GET | /resolve?repo=&filename= | — | Resolve HuggingFace filename patterns (cached). |
Production deploy (Railway)
The api/railway.toml is set up for Railway. Required in prod:
- Mounted volume on
DATA_DIRso the SQLite files survive deploys. ALLOWED_ORIGIN=https://comfyforge.app(or your frontend domain).COOKIE_DOMAIN=.comfyforge.appso the session cookie spans subdomains.NODE_ENV=productionso cookies go out withSecure.
How cold-start works
The base image tcpassos/comfyui-cloud:latest (source in comfyui-docker) supports two modes:
- Runtime: container boots, reads
CONFIG_URL, downloads nodes/models on boot. Slow cold-start, zero pre-build. - Baked: the CLI/web runs
comfy-provisioninside the build, pre-installing everything into/opt/preinstalled. At boot the entrypoint just does a quick merge. Cold-start in seconds.
The PROVISION_TARGET=/opt/preinstalled flag in Dockerfile.bake is what enables the baked mode.
Repo layout
comfyforge/
├── src/ # CLI (init + bake)
│ ├── index.ts # commander setup
│ ├── wizard.ts # init flow
│ ├── bake.ts # bake flow
│ └── lib/
│ ├── configHash.ts # deterministic hash (node:crypto)
│ ├── bakeTemplates.ts
│ └── secretsDetect.ts
├── web/ # React + Vite
│ └── src/lib/ # same libs, browser version (crypto.subtle)
└── api/ # Express + SQLite — auth, library, gallery, permalinks
└── src/ # GitHub OAuth, library CRUD, public gallery, HF resolverBuild from source
git clone https://github.com/tcpassos/comfyforge.git
cd comfyforge
npm install
npm run build # CLI (tsc → dist/)
cd web && npm run build # web (tsc + vite)Roadmap
- [x] Layer 0 — web app generates a recipe zip for local build
- [x] Layer 1 —
bakeCLI that runsdocker buildxdirectly - [ ] Layer 2 — "Bake on GitHub Actions" button (generated workflow + dispatch)
- [ ] Layer 3 — ephemeral build pod on RunPod (direct uploads, no intermediate download)
License
MIT © tcpassos
