Skip to main content

comfy-deploy

Run a Comfy Build release as a serverless deployment with comfy-cli. Use whenever the user wants to deploy, serve, host, or expose a ComfyUI build as an endpoint, scale or stop workers, submit a workflow to a deployment, check whether a deployment is healthy or running a stale release, or work out why one is still costing money. Covers `comfy deploy up / promote / rollback / history / cancel / run / status / scale / stop / start / delete / ls / show / logs / events / refs`. Assumes a green release already exists — `comfy-build` is the skill that produces one.

Source facts

Repository
Comfy-Org/comfy-cli
Last source activity
October 26, 2026 at 16:54
Detected SKILL.md language
English
Stars
1,046
Forks
173

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
comfy-deploy
description
Run a Comfy Build release as a serverless deployment with comfy-cli. Use whenever the user wants to deploy, serve, host, or expose a ComfyUI build as an endpoint, scale or stop workers, submit a workflow to a deployment, check whether a deployment is healthy or running a stale release, or work out why one is still costing money. Covers `comfy deploy up / promote / rollback / history / cancel / run / status / scale / stop / start / delete / ls / show / logs / events / refs`. Assumes a green release already exists — `comfy-build` is the skill that produces one.
# comfy-deploy The commands here are the `comfy deploy` group from [comfy-cli](https://github.com/Comfy-Org/comfy-cli). Everything in it needs `comfy cloud login`, or a workspace API key in `COMFY_CLOUD_API_KEY`, which a CI job uses in place of a sign-in and which wins over a stored sign-in; a command answers `deploy_not_signed_in` when it has neither, or when the key is refused. **Deploying spends money continuously, not once.** Building does not: it is capped by counts — how many builds and releases a workspace holds — and metered by nothing, so a finished build costs nothing to leave lying there. A deployment holds compute until something stops it: `up` and `run` create paid compute, `start` resumes it, and `scale --min` sets the standing charge which can approach 0 with a `scale --min 0`. Every other verb reads, or gives compute back. **This skill's surface is the one that bills by time.** ## What you are working with - **A deployment runs exactly one release.** It is keyed to a release id, not to a Build. This single fact drives most of what follows. - **The Build is the addressing scheme.** Nearly every command takes the install directory or spec path as its argument and defaults to the current directory, reading the Build id out of `comfy-build.yaml`. `--deployment <name or id>` overrides that whenever the Build has more than one, and `--deployment <build>/<name>` reaches a deployment from outside its folder. - **`comfy-build` produced the release.** If there is no green release yet, that skill is the one to run: `comfy skills show comfy-build`. - **The failure material is a reference skill.** When a command is refused or a deployment will not come up, read `comfy skills show comfy-deploy-failures` rather than guessing at a code. ## The command surface ``` up Create or reconcile a deployment for the selected Build release. SPENDS promote Move TARGET onto the release SOURCE serves, keeping its URL. SPENDS rollback Move a deployment back to an earlier release, keeping its URL. SPENDS rename Give a deployment a new name, keeping its id and URL. history The releases a deployment ran, newest first, and what moved it. cancel End the update a deployment waits on; it keeps its release. run Submit an API-format workflow to a ready deployment. SPENDS status Deployment health, release freshness, and serving activity. scale Edit worker bounds, or GPU/region on a stopped deployment. stop Pause a deployment, retaining its endpoint and staged models. start Resume a stopped or failed deployment. SPENDS delete Enqueue teardown and soft-delete the record. ls List deployments for this Build, or the whole workspace. show One deployment's raw control-plane record. logs One deployment's captured ComfyUI log snapshot. events One deployment's status events in server order. refs compute — deployable regions and GPU classes with availability. ``` ## The path ```shell comfy build release show # confirm deployable: true comfy deploy refs compute # read the real GPU/region pairs comfy deploy up <dir> --gpu <class> --region <region> --min 0 --max 1 comfy deploy status <dir> comfy deploy run <dir> --workflow <api-workflow>.json comfy deploy stop <dir> # when the user is done ``` Only the first two lines are free. Disclose before the third — see *Before you deploy*. ## The cost model, which is the whole risk **Whether `up` on a new release moves the existing deployment or adds a second one depends on the workspace.** The output says which: a deployment that carries `revision` is in a workspace with deployment updates on. - **Updates on:** `up` on a new release **moves the Build's one deployment onto it**, keeping its id and URL, and reports `previousRelease`. The old release keeps serving until the new one is ready. With two or more deployments, `up` refuses with `deploy_ambiguous_deployment`, listing each one's name, release and status, until `--deployment <name>` names one; `--create` adds a separate deployment instead. If the new release fails to come up, `up` exits 1 with `deploy_update_failed` and the old release still serves. On a deploy service that lets a newer update replace a waiting one, a newer `up`, `promote` or `rollback` while one waits replaces it: the watch of the first exits 1 with `deploy_update_replaced`, naming the newer release. An older service refuses the newer move while one waits. - **Updates off:** a deployment is matched by release id, so cutting a new release and running `up` again does **not** move the existing deployment forward. It creates a **second** deployment, and the first keeps running and keeps billing. The CLI tells you this: `up` returns a `supersedes` array naming every other live deployment of this Build still holding compute, with its id, status and release version, and prints a "still running and billing" warning for each (on stderr under `--json`). **Read it and act on it.** An empty array means nothing else is running; a non-empty one is a bill the user has not agreed to. ```shell comfy deploy up <dir> # note `supersedes` in the output comfy deploy stop --deployment <old-id> ``` **A deployment holds compute in `queued`, `provisioning`, `starting`, `ready` and `unhealthy`.** Those five are the billing states. `unhealthy` is the trap among them: it is running and broken, so it costs exactly what `ready` costs. **`stop_failed` may still be billing.** The stop did not take. The CLI warns and tells you to run `comfy deploy stop --deployment <id>` again — do that rather than assuming the compute is gone, and say so to the user. **`--min` is the standing charge.** `--min 0` lets the deployment scale to zero between jobs and pay a cold start on the next one; `--min 1` keeps a worker warm and pays for it whether or not anything is submitted. Default is `0`/`1` on a new deployment. Both bounds move together: `--min` and `--max` must be supplied as a pair, `--min` accepts 0–20 and `--max` 1–20. ## Deployment states | Status | Holds compute | Meaning | | --- | --- | --- | | `queued` | yes | Waiting for provisioning | | `provisioning` | yes | Allocating resources | | `starting` | yes | Launching ComfyUI | | `ready` | yes | Accepting jobs — the only state `run` will submit to | | `unhealthy` | yes | Running and degraded; still billing | | `stopping` | no | Shutting down | | `stopped` | no | Stopped cleanly | | `stop_failed` | **maybe** | Stop did not take; retry it | | `failed` | no | Permanent failure | `up` follows the deployment by default; pass `--no-watch` to return as soon as it is accepted. `status` waits only when asked, with `--watch`. Either way the wait ends at `ready`, `unhealthy`, `failed`, `stopped` or `stop_failed`. `unhealthy` only ever follows `ready`, so the deployment already came up: `up` reports it as not ok (`deploy_status_terminal`, exit 1) because it is billing without serving, and `status` reports it as recoverable. Through `queued`, `provisioning`, `starting` and `stopping` it keeps waiting. While the status is `provisioning` or `starting` the deployment carries a `progress` object, and `status --json` returns it as `data.progress`: `step` (`staging_models`, `creating_endpoint`, `waiting_for_worker`) and, while models are copied onto the deployment's storage, `modelsDone` / `modelsTotal`, `bytesDone` / `bytesTotal`, `currentModel`, `bytesPerSecond`, `etaSeconds` and `placingModels`. Under `--watch` the same object arrives as `deploy_progress` events, one per new sample (the service rewrites it about every three seconds in every step): on **stderr** under `--json`, on stdout under `--json-stream`. Relay those numbers instead of "still provisioning". Things to read correctly: - `bytesTotal` absent means nobody measured the release, so there is no time left to quote; `0` means every model was already in place. - `bytesTotalIsFloor: true` means "at least this much", with no `etaSeconds`. - `placingModels: true` means every byte is in and staging is finishing: the models are being put in place and, on Modal, the release image built beside the upload may still be building, for up to about 4 minutes after the last byte. The step stays `staging_models` with `bytesDone` equal to `bytesTotal` and no rate, and the progress line adds "finishing: putting the models in place" on every provider. That is not a stall, even once `etaSeconds` runs out and is dropped; never infer it from the bytes alone. - `etaSeconds` covers staging only. Creating the endpoint and the first worker's cold start come after it. - `attempt` above 1 means the step was restarted, and `bytesDone` started again. - `stale: true` on an event means the service has not rewritten the sample for a minute. Its writes are best-effort, so that is not evidence the deploy stopped; the status is still the verdict. - How long a step has run is now minus `startedAt`. `updatedAt` only says how fresh the sample is, and it moves every few seconds in every step. - No `progress` at all is an older service, or a status other than the two above. Interrupting the wait leaves the deployment coming up on the service's side; `comfy deploy status --deployment <id> --watch` attaches again. A deploy API that stops answering (a 5xx or a dropped connection, as during its own rollout) is retried for about a minute; if it is still down, the wait ends with `deploy_watch_lost` and **exit 75**, not 1. That is not a failed deployment: attach again with the command in the hint rather than redeploying. `status` also reports **why** a deployment stopped, as `stopReason`: `user`, `credits`, or `policy`. `credits` is a billing problem and not something a retry fixes — say so rather than restarting into the same wall. ## `comfy deploy up` ```shell comfy deploy up [PATH] --gpu <class> --region <region> [--min N --max N] [--release <id>] [--deployment <name|id>] [--create [--name <name>]] [--no-editor] [--no-watch] comfy deploy rename [PATH] <name> [--deployment <name|id>] ``` - **With deployment updates on, it moves the existing deployment** (see *The cost model*), a stopped or failed one included, since the move starts the new release for it. A move keeps gpu and region, refuses a `stopping` or `stop_failed` deployment with `deploy_conflict`, and applies `--min`/`--max` only after the move lands, so with `--no-watch` they are refused. A bounds edit the service then refuses is reported as bounds that had no effect. - **`--create` is not idempotent:** every run adds one more deployment, so after a lost response read `comfy deploy ls` before running it again. - **`--name` names the deployment a create makes**, the Build's first included; without it comfy-deploy names the first `production` and later ones `deployment-N`. A name is 1 to 40 lowercase letters, digits and hyphens, starting and ending with a letter or digit, never starting with `dep-`, and unique among the Build's live deployments (`deploy_invalid_name`, `deploy_name_taken`). The same `up --name` run again finds the deployment holding that name. `up` refuses `--name` when it would update a deployment holding another name: `comfy deploy rename` names an existing one, and without `--deployment` renames the Build's only one. - **Pass `--no-editor` for a deployment only called through the API**, never opened in the editor: without it comfy-deploy gives a new deployment a CPU server for the editor, running beside it. With neither flag an existing deployment keeps what it has, on or off. On an existing deployment the flag goes with the move, or alone, starting no new copy; `--editor` turns the server back on. A value comfy-deploy does not confirm, a failed read to confirm it included, exits 1 with `deploy_editor_unchanged`, and the rest of what `up` did stands: `data` and `changed` still say what it did. - **It selects the newest deployable release of the Build** unless `--release` names one. `deployable` means a `linux/nvidia` artifact reached `ready` with an image ref; a release cut only for other targets is refused with `deploy_no_deployable_release`, whose message distinguishes "no releases at all" from "releases, none deployable". - **`--gpu` and `--region` are required for a new deployment**, and it prompts for them interactively. Under `--json` an omission is `deploy_missing_input`. Take the values from `comfy deploy refs compute`, which lists every location with its level (a whole country such as `us` down to one datacenter), its parent, its GPU classes, VRAM and availability. A wider location is a valid `--region`, and some GPU classes are sold only on one. A blank availability means the service gave no hint for that row, not that it has no stock. Do not invent a class name. - **Availability is volatile, so re-read `refs compute` immediately before `up`, never from an earlier plan.** A region offering RTX PRO 6000 at planning time had none an hour later, and `up` refused with `deploy_compute_unavailable`. The reading is a snapshot with a short shelf life, not a catalog. - **GPU and region are immutable on a live deployment.** Passing a different one is `deploy_immutable_compute`. Changing them is `stop` → `scale --gpu/--region` → `start`. - **It is idempotent per release.** The create is keyed by a deterministic idempotency key over build, release and delete-generation, so a retry after a timeout returns the same deployment rather than a second billable one. - **An omitted bound keeps the live value**, so re-running `up` after a release does not silently unscale. A bound you passed that would not change anything is reported back as dropped. - **It restarts a `stopped` or `failed` deployment** for that release instead of creating another. - **A create carries the service's estimate** of how long the deployment takes to come up, as `estimate` in the output: `etaSecondsLow`/`etaSecondsHigh` until ready and `bytesToFetch` of models to download, with `atLeast: true` when some models have no recorded size. A person at a terminal sees it before `up` starts following the deployment. Under `--json` it arrives with the one envelope, after the watch, so to relay it before a long wait run `up --no-watch`, tell the user, then follow with `comfy deploy status --watch`. It is a range, not a promise, and it stops at ready: the first run can still wait for a worker to start. Absent on a restart, an edit, when the service gave none, or when the service has the estimate switched off; that is not an error, so never retry for it or mention its absence. ## `comfy deploy promote` ```shell comfy deploy promote SOURCE TARGET [--no-watch] ``` - **It moves deployment TARGET onto the release deployment SOURCE serves**, keeping TARGET's id and URL: test on a staging deployment, then promote it to production. The service resolves SOURCE's release itself. - **SOURCE and TARGET each take a name or an id**: a bare name in the Build's folder, or `<build>/<name>` anywhere, the Build given by its name or id. - **It needs deployment updates on.** Without them it refuses with `deploy_updates_unavailable`; use `comfy deploy up --create` instead. - **It follows the move like `up` does**: exit 1 with `deploy_update_failed` when the new release does not come up, and TARGET still serves its old one, or with `deploy_update_replaced` when a newer update replaced it. ## `comfy deploy rollback` and `comfy deploy history` ```shell comfy deploy rollback [PATH] [--deployment <name|id>] [--to vN|<release-id>] [--no-watch] comfy deploy history [PATH] [--deployment <name|id>] ``` - **`rollback` moves the deployment back to the release before its current one**, keeping its id and URL, so a second `rollback` undoes the first. `--to vN` returns to the latest earlier revision that ran vN, and refuses a release the deployment never ran. A release the Build no longer lists has no version to name; pass its id from `history` instead. - **`--json --no-watch` says `waiting: true`** while the earlier release's copy starts; the deployment serves `previousRelease` until it lands. - **It picks the deployment as `up` does**: the one `--deployment` names, else the Build's only running one, refusing two with `deploy_ambiguous_deployment`. - **It follows the move like `up` does**: exit 1 with `deploy_update_failed` when the earlier release does not come back up, or with `deploy_update_replaced` when a newer update replaced it. - **`history` lists each revision newest first**, the current one marked `*`: its release version, what made it (`create`, `update`, `rollback`), who and when. Run it before `rollback --to` to see what is there to return to. - **Both need deployment updates on** (`deploy_updates_unavailable` otherwise). ## `comfy deploy cancel` ```shell comfy deploy cancel [PATH] [--deployment <name|id>] ``` - **It ends the update a deployment waits on**, an `up`, `promote` or `rollback` whose release is still coming up, and stops that copy. The deployment keeps serving the release it served, at the same revision. - **It names what it ended:** `--json` carries `cancelledUpdate`, the release that update moved to, its `baseRevision` and `kind`. That is whichever update waited when the service took the cancel, which can be a newer one than you saw, so read it rather than assuming. - **Nothing waiting is not an error:** it exits 0 with `changed: false` and `cancelledUpdate: null`, as a second cancel does. - **It picks the deployment as `rollback` does.** A deploy service too old for cancel refuses with `deploy_updates_unavailable`; wait for the update to land or fail instead. - **A cancelled move back to a kept release leaves that release as it was:** one you stopped stays stopped and yours, and one on its way out still goes. ## `comfy deploy run` ```shell comfy deploy run [PATH] --workflow <api-workflow>.json [--output-dir outputs] [--timeout <s>] [--no-wait] [--no-upload] [--asset-root <dir>] ``` - **API format only.** A UI-format export (the one with `nodes` and `links`) is refused locally with `deploy_workflow_format_ui` before anything is submitted; use ComfyUI's *File → Export (API)*. An empty object is `deploy_workflow_empty`, and a JSON list, string or number is `deploy_workflow_not_api_format`. None of these cost anything.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub