ICP CLI
What This Is
The
command-line tool builds and deploys applications on the Internet Computer. It replaces the legacy
tool with YAML configuration, a recipe system for reusable build templates, and an environment model that separates deployment targets from network connections. Never use
— always use
.
Prerequisites
- For Rust canisters:
rustup target add wasm32-unknown-unknown
- For Motoko canisters: and version defined in (templates include this; for manual projects add with )
- For frontend assets: Node.js >= 20
Common Pitfalls
-
Using instead of . The
tool is legacy. All commands have
equivalents — see the migration table below. Never generate
commands or reference
documentation. Configuration uses
, not
— and the structure differs: canisters are an array of objects, not a keyed object.
-
Using to deploy to mainnet. icp-cli uses environments, not direct network targeting. The correct flag is
(short for
).
bash
# Wrong
icp deploy --network ic
# Correct
icp deploy -e ic
Note:
/
targets a network directly and works with canister IDs (principals). Use
/
when referencing canisters by name. For token and cycles operations, use
since they don't reference project canisters.
-
Using a recipe without a version pin. Always pin recipe versions to avoid breaking changes. Unpinned recipes resolve to
which can change at any time. Official recipes are hosted at
dfinity/icp-cli-recipes.
yaml
# Wrong — unpinned, may break
recipe:
type: "@dfinity/rust"
# Correct — pinned version
recipe:
type: "@dfinity/rust@v3.2.0"
-
Writing manual build steps when a recipe exists. Official recipes handle Rust, Motoko, and asset canister builds. Use them instead of writing shell commands:
yaml
# Unnecessary — use a recipe instead
build:
steps:
- type: script
commands:
- cargo build --target wasm32-unknown-unknown --release
- cp target/.../backend.wasm "$ICP_WASM_OUTPUT_PATH"
# Preferred
recipe:
type: "@dfinity/rust@v3.2.0"
configuration:
package: backend
-
Not committing to version control. Mainnet canister IDs are stored in
.icp/data/mappings/<environment>.ids.json
. Losing this file means losing the mapping between canister names and on-chain IDs. Always commit
— never delete it. Add
to
(it is ephemeral and rebuilt automatically).
-
Using instead of . The dfx command
became
. Similarly,
dfx identity get-principal
became
, and
became
.
-
Confusing networks and environments. A network is a connection endpoint (URL). An environment combines a network + canisters + settings. You deploy to environments (
), not networks. Multiple environments can target the same network with different settings (e.g., staging and production both on
).
-
Forgetting that local networks are project-local. Unlike dfx which runs one shared global network, icp-cli runs a local network per project. You must run
in your project directory before deploying locally. The local network auto-starts with system canisters and seeds accounts with ICP and cycles.
-
Not specifying build commands for asset canisters. dfx automatically runs
for asset canisters. icp-cli requires explicit build commands in the recipe configuration:
yaml
canisters:
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.1.0"
configuration:
dir: dist
build:
- npm install
- npm run build
-
Expecting or with canister IDs. dfx writes canister IDs to a
file (
) via
. icp-cli does not generate
files. Instead, it injects canister IDs as environment variables (
PUBLIC_CANISTER_ID:<name>
) directly into canisters during
. Frontends read these from the
cookie set by the asset canister. Remove
from your config and any code that reads
from
— use the
cookie instead (see Canister Environment Variables below).
-
Expecting for TypeScript bindings. icp-cli does not have a
equivalent. Use
(a Vite plugin) to generate TypeScript bindings from
files at build time. The
file must exist on disk — either commit it to the repo, or generate it with
first (recipes auto-generate it when
is not specified). See Binding Generation below.
-
Misunderstanding Candid file generation with recipes. When using the Rust or Motoko recipe:
- If is specified: the file must already exist (checked in or manually created). The recipe uses it as-is and does not generate one.
- If is omitted: the recipe auto-generates the file from the compiled WASM (via for Rust, for Motoko). The generated file is placed in the build cache, not at a predictable project path.
For projects that need a
file on disk (e.g., for
), the recommended pattern is: generate the
file once, commit it, and specify
in the recipe config. To generate it manually:
Rust — build the WASM first, then extract the Candid interface:
bash
cargo install candid-extractor # one-time setup
icp build backend
candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/backend.did
Motoko — use
directly with the
flag:
bash
$(mops toolchain bin moc) --idl $(mops sources) -o backend/backend.did backend/app.mo
How It Works
Project Creation
scaffolds projects from templates. Without flags, an interactive prompt launches. For scripted or non-interactive use, pass
and
flags directly. Available templates and options:
dfinity/icp-cli-templates.
Build → Deploy → Sync
text
Source Code → [Build] → WASM → [Deploy] → Running Canister → [Sync] → Configured State
runs all three phases in sequence:
- Build — Compile canisters to WASM (via recipes or explicit build steps)
- Deploy — Create canisters (if new), apply settings, install WASM
- Sync — Post-deployment operations (e.g., upload assets to asset canisters)
Run phases separately for more control:
bash
icp build # Build only
icp deploy # Full pipeline (build + deploy + sync)
icp sync my-canister # Sync only (e.g., re-upload assets)
Environments and Networks
Two implicit environments are always available:
| Environment | Network | Purpose |
|---|
| (managed, localhost:8000) | Local development |
| (connected, https://icp-api.io) | Mainnet production |
The
network is protected and cannot be overridden.
Custom environments enable multiple deployment targets on the same network:
yaml
environments:
- name: staging
network: ic
canisters: [frontend, backend]
settings:
backend:
compute_allocation: 5
- name: production
network: ic
canisters: [frontend, backend]
settings:
backend:
compute_allocation: 20
freezing_threshold: 7776000
Install Modes
bash
icp deploy # Auto: install new, upgrade existing (default)
icp deploy --mode upgrade # Preserve state, run upgrade hooks
icp deploy --mode reinstall # Clear all state (dangerous)
Configuration
Rust canister
yaml
canisters:
- name: backend
recipe:
type: "@dfinity/rust@v3.2.0"
configuration:
package: backend
candid: backend.did # optional — if specified, file must exist (auto-generated when omitted)
Motoko canister
yaml
canisters:
- name: backend
recipe:
type: "@dfinity/motoko@v4.1.0"
configuration:
main: src/backend/main.mo
candid: backend.did # optional — if specified, file must exist (auto-generated when omitted)
Asset canister (frontend)
yaml
canisters:
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.1.0"
configuration:
dir: dist
build:
- npm install
- npm run build
For multi-canister projects, list all canisters in the same
array. icp-cli builds them in parallel. There is no
field — use Canister Environment Variables for inter-canister communication.
Custom build steps (no recipe)
When not using a recipe, only
,
,
,
, and
are valid canister-level fields. There are no
,
, or
fields — handle these in the build script instead:
- WASM output: copy the final WASM to
- Candid metadata: use to embed metadata
- Candid file: the file is referenced only in the command, not as a YAML field
yaml
canisters:
- name: backend
build:
steps:
- type: script
commands:
- cargo build --target wasm32-unknown-unknown --release
- cp target/wasm32-unknown-unknown/release/backend.wasm "$ICP_WASM_OUTPUT_PATH"
- ic-wasm "$ICP_WASM_OUTPUT_PATH" -o "$ICP_WASM_OUTPUT_PATH" metadata candid:service -f backend/backend.did -v public --keep-name-section
Available recipes
| Recipe | Purpose |
|---|
| Rust canisters with Cargo |
| Motoko canisters |
| Asset canisters for static files |
| Pre-compiled WASM files |
Use
to see the effective configuration after recipe expansion.
Canister Environment Variables
icp-cli automatically injects all canister IDs as environment variables during
. Variables are formatted as
PUBLIC_CANISTER_ID:<canister-name>
and injected into every canister in the environment.
Frontend → Backend (reading canister IDs in JavaScript):
Asset canisters expose injected variables through a cookie named
, set on all HTML responses. Use
to read it:
js
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
const canisterEnv = safeGetCanisterEnv();
const backendId = canisterEnv?.["PUBLIC_CANISTER_ID:backend"];
Backend → Backend (reading canister IDs in canister code):
- Rust:
ic_cdk::api::env_var_value("PUBLIC_CANISTER_ID:other_canister")
- Motoko (motoko-core v2.1.0+):
motoko
import Runtime "mo:core/Runtime";
let otherId = Runtime.envVar("PUBLIC_CANISTER_ID:other_canister");
Note: variables are only updated for canisters being deployed. When adding a new canister, run
(without specifying a canister name) to update all canisters with the complete ID set.
Binding Generation
icp-cli does not have a built-in
command. Use
to generate TypeScript bindings from
files.
Vite plugin (recommended for Vite-based frontend projects):
js
// vite.config.js
import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite";
export default defineConfig({
plugins: [
// Add one icpBindgen() call per canister the frontend needs to access
icpBindgen({
didFile: "../backend/backend.did",
outDir: "./src/bindings/backend",
}),
icpBindgen({
didFile: "../other/other.did",
outDir: "./src/bindings/other",
}),
],
});
Each
instance generates a
function in its
. Add
to
.
Creating actors from bindings — connect the generated bindings with the
cookie:
js
// src/actor.js
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
import { createActor } from "./bindings/backend";
// For additional canisters: import { createActor as createOther } from "./bindings/other";
const canisterEnv = safeGetCanisterEnv();
const agentOptions = {
host: window.location.origin,
rootKey: canisterEnv?.IC_ROOT_KEY,
};
export const backend = createActor(
canisterEnv?.["PUBLIC_CANISTER_ID:backend"],
{ agentOptions }
);
// Repeat for each canister: createOther(canisterEnv?.["PUBLIC_CANISTER_ID:other"], { agentOptions })
Non-Vite frontends — use the
CLI to generate bindings manually:
bash
npx @icp-sdk/bindgen --did ../backend/backend.did --out ./src/bindings/backend
Requirements:
- The file must exist on disk. If using a recipe with specified, the file must be committed. If is omitted, run first to auto-generate it.
- generates code that depends on . Projects using must upgrade to + . This is not optional — there is no way to generate TypeScript bindings with icp-cli while staying on .
Dev Server Configuration (Vite)
In development, the Vite dev server must simulate the
cookie that the asset canister provides in production. Query the local network for the root key, canister IDs, and API URL:
js
// vite.config.js
import { execSync } from "child_process";
const environment = process.env.ICP_ENVIRONMENT || "local";
// List all backend canisters the frontend needs to access
const CANISTER_NAMES = ["backend", "other"];
function getCanisterId(name) {
// `-i` makes the command return only the identity of the canister
return execSync(`icp canister status ${name} -e ${environment} -i`, {
encoding: "utf-8", stdio: "pipe",
}).trim();
}
function getDevServerConfig() {
const networkStatus = JSON.parse(
execSync(`icp network status -e ${environment} --json`, {
encoding: "utf-8",
})
);
const canisterParams = CANISTER_NAMES
.map((name) => `PUBLIC_CANISTER_ID:${name}=${getCanisterId(name)}`)
.join("&");
return {
headers: {
"Set-Cookie": `ic_env=${encodeURIComponent(
`${canisterParams}&ic_root_key=${networkStatus.root_key}`
)}; SameSite=Lax;`,
},
proxy: {
"/api": { target: networkStatus.api_url, changeOrigin: true },
},
};
}
Key differences from dfx:
- The proxy target and root key come from
icp network status --json
(no hardcoded ports)
- Canister IDs come from
icp canister status <name> -e <env> -i
(no file)
- The cookie replaces dfx's environment variables
- lets the dev server target any environment (local, staging, ic)
dfx → icp Migration
Local network port change
dfx serves the local network on port
. icp-cli uses port
. When migrating, search the project for hardcoded references to
(or
) and update them to
. Better yet, use
icp network status --json
to get the
dynamically (see Dev Server Configuration above). Common locations to check:
- Vite/webpack proxy configs (e.g., )
- README documentation
- Test fixtures and scripts
Remove file and
dfx generates a
file with
variables via
in
. icp-cli does not use
files for canister IDs — remove
from config and delete any dfx-generated
file. Also remove dfx-specific environment variables from
files (e.g.,
,
).
Replace code that reads
process.env.CANISTER_ID_*
with the
cookie pattern (see Canister Environment Variables above).
Frontend package migration
Since
generates code that depends on
, projects with TypeScript bindings
must upgrade from
packages. This is not optional —
does not exist in icp-cli, and
is the only supported way to generate bindings.
| Remove | Replace with |
|---|
| |
| |
| |
| (declarations) | (Vite plugin or CLI) |
| Not needed — use cookie |
| (generated by dfx) | (generated by ) |
Steps:
npm uninstall @dfinity/agent @dfinity/candid @dfinity/principal vite-plugin-environment
npm install @icp-sdk/core @icp-sdk/bindgen
- Delete (dfx-generated bindings)
- Add to
- Commit the file(s) used by bindgen
- Add to (see Binding Generation above)
- Replace actor setup code: use from + from generated bindings (see Creating actors from bindings above)
- Remove
process.env.CANISTER_ID_*
references — use the cookie instead
Command mapping
| Task | dfx | icp |
|---|
| Create project | | |
| Start local network | | |
| Stop local network | | |
| Build | | |
| Deploy all | | |
| Deploy to mainnet | | |
| Call canister | dfx canister call X method '(args)'
| icp canister call X method '(args)'
|
| Get canister ID | | icp canister status X --id-only
|
| Canister status | | |
| List canisters | | |
| Create identity | | |
| Set default identity | | icp identity default my_id
|
| Show principal | dfx identity get-principal
| |
| Export identity | dfx identity export my_id
| icp identity export my_id
|
| Delete identity | dfx identity remove my_id
| icp identity delete my_id
|
| Get account ID | | |
| Check ICP balance | | |
| Check cycles | | |
Configuration mapping
| dfx.json | icp.yaml |
|---|
| recipe.type: "@dfinity/rust@v3.2.0"
|
| recipe.type: "@dfinity/motoko@v4.1.0"
|
| recipe.type: "@dfinity/asset-canister@v2.1.0"
|
| recipe.configuration.package: X
|
| recipe.configuration.candid: X
|
| recipe.configuration.main: X
|
| recipe.configuration.dir: dist
|
| Not needed — use Canister Environment Variables |
"output_env_file": ".env"
| Not needed — use cookie |
| Vite plugin |
| |
Identity migration
bash
# Export from dfx, import to icp-cli
dfx identity export my-identity > /tmp/my-identity.pem
icp identity import my-identity --from-pem /tmp/my-identity.pem
rm /tmp/my-identity.pem
# Verify principals match
dfx identity get-principal --identity my-identity
icp identity principal --identity my-identity
Canister ID migration
If you have existing mainnet canisters managed by dfx, migrate the IDs from
to icp-cli's mapping file:
bash
# Get IDs from dfx
dfx canister id frontend --network ic
dfx canister id backend --network ic
# Create mapping file for icp-cli
mkdir -p .icp/data/mappings
cat > .icp/data/mappings/ic.ids.json << 'EOF'
{
"frontend": "xxxxx-xxxxx-xxxxx-xxxxx-cai",
"backend": "yyyyy-yyyyy-yyyyy-yyyyy-cai"
}
EOF
# Delete the dfx canister ID file — icp-cli uses .icp/data/mappings/ instead
rm -f canister_ids.json
# Commit to version control
git add .icp/data/
Post-Migration Verification
After migrating a project from dfx to icp-cli, verify the following:
- Deleted files: and no longer exist
- Created files: exists.
.icp/data/mappings/ic.ids.json
exists and is committed (if project has mainnet canisters)
- : contains , does not contain
- No stale port references: search the codebase for — there should be zero matches
- No dfx env patterns: search for , , — there should be zero matches in config and source files
- Frontend packages (if project has TypeScript bindings): is not in , and are. is deleted, is in
- Candid files: files used by are committed
- Build succeeds: completes without errors
- Config is correct: displays the expected expanded configuration
- README: references commands (not ), says "local network" (not "replica"), shows correct port