| name | deploy-to-cloud-engine |
| description | Deploys a built Internet Computer project to a cloud engine (OpenCloud): link the console identity with `icp identity link web` (defaults to https://opencloud.org; a delegation handoff covers sandboxes the browser cannot reach), run `icp deploy` on the engine's subnet, tag canisters with `__META_*` for a named console app, bake version metadata (service:git:sha) into the wasm, and deploy the proxy an app needs for Bitcoin/Ethereum signing, VetKeys or exchange rates, card-funded from the console or self-deployed via `icp new --subfolder proxy`. Use when shipping to a cloud engine; on mention of OpenCloud, an engine subnet id, or linking the icp CLI; when sign-in never completes from a sandbox; when naming, versioning or giving an icon to a console app; when a proxy must be deployed, funded or topped up (failing proxied calls: cloud-engine-canisters); or which balance to top up (subscription, reserve, proxy). Do NOT use for a mainnet deploy with no engine (icp-cli) or canister logic (cloud-engine-canisters). |
| license | Apache-2.0 |
| compatibility | icp-cli >= 0.3.0 (deploy/identity commands verified against 0.3.0 and 1.0.2, proxy and cycles commands against 1.0.2 and 1.3.0; the delegation handoff needs `icp identity delegation`, present in 1.0.x; the `proxy` project template needs `icp new --subfolder`), a cloud engine console account, a browser for the Internet Identity sign-in, and a saved payment method for the console-funded proxy |
| metadata | {"title":"Deploy to Cloud Engine","category":"CloudEngine"} |
Deploy to Cloud Engine
What This Is
A cloud engine is a user-owned slice of Internet Computer capacity, administered from a web console (by default https://opencloud.org). Each engine runs on a single subnet. This skill takes a project that already builds and gets it deployed onto that engine, from a coding agent.
This skill only covers the cloud-engine-specific steps: linking the CLI to the engine's console identity, and a subnet-targeted deploy. For everything else about the CLI (icp.yaml, recipes, environments, bindings, identities), load the icp-cli skill.
Before running any icp command you are unsure of, run icp <subcommand> --help (e.g. icp identity link --help, icp deploy --help) to confirm the command and flags exist. Do not infer flags. Authoritative reference: https://cli.internetcomputer.org/llms.txt
What You Need
Two values. Look for them first in icp.yaml or earlier in the conversation. One has a default; the other you must ask for:
- Console origin — the URL the user signs in to their cloud engine console with. Defaults to
https://opencloud.org (the main OpenCloud console). It is used as the --auth origin in Step 1 so the linked CLI identity derives the same principal that administers the engine. Use the default, but say so and give the user a chance to override before linking:
- Say: "I'll link the CLI against
https://opencloud.org, the default console. If you sign in to your engine console at a different URL, tell me now."
- Only use a different origin when the user names one — never substitute another URL on your own; the
--auth origin determines the derived principal (see Pitfall 2).
- Subnet id: the subnet the engine deploys to, required by
icp deploy --subnet. There is no default; never guess it. The user finds it on the engine's Settings page in the console (under the engine's identifiers), or copies it from the console's command palette. If absent, ask and do not proceed without it:
- Ask: "What is your engine's subnet id? It is on your engine's Settings page in the console."
Record both so you do not re-ask within the session.
Prerequisites
icp on $PATH — see the icp-cli skill to install. Verify with icp --version (this skill's commands are verified against 0.3.0 and 1.0.2). If the installed version differs, confirm the flag set with icp <cmd> --help before running — flags have changed across major versions.
- A project that already builds. If it does not build or package yet, set that up first (see the
icp-cli skill), then return here.
- macOS:
icp stores its data under ~/Library/Application Support/org.dfinity.icp-cli/. If the shell cannot write there (Operation not permitted from macOS TCC, e.g. when commands run through a bridge), redirect the data to an unprotected path for the session — HOME=/tmp/icp-home icp … — and keep that same HOME on every subsequent icp command, or the later commands will not see the linked identity.
Step 1 — Link the CLI to your engine identity (once per machine)
The CLI must sign as the same identity that administers the engine — that is the principal you log in to the console with.
Step 1.0 — First determine WHERE the CLI runs relative to the browser
icp identity link web completes the sign-in via a redirect to http://127.0.0.1:<port> on the machine where the CLI runs. The browser in which the user completes the Internet Identity sign-in must be able to reach that loopback address. Determine the environment before linking:
- CLI and browser on the same machine, with network (local development, or a terminal-integrated agent on the user's own machine) → use the normal link flow below.
- CLI in a remote or isolated sandbox, with network (the agent runs in the cloud; the user's browser is on a different machine) → the normal flow cannot work: the sign-in URL carries a
callback=http://127.0.0.1:<port> bound to the sandbox, the user's browser resolves 127.0.0.1 to its own machine, and the delegation never reaches the CLI. Later commands then fail with authorization errors. Use the delegation handoff below instead.
- CLI shell with no network at all (e.g. a sandboxed device bridge on the user's own machine: DNS blocked, HTTPS requests fail, writes limited to
/tmp) → no icp network command can run there — not the link, and not icp deploy either. The delegation handoff does not help: it only moves signing authority, not network access. Do not retry, tunnel, or proxy. Prepare the project and hand the user one script to run in their real terminal — see "No-network CLI host" below.
When in doubt, probe before linking: curl -sI https://<console-origin> failing (or DNS not resolving) from the CLI shell means the no-network case.
Never present a sandbox 127.0.0.1 URL to the user as something to open in their browser — it is unreachable from their machine. And do not invent a headless flag on icp identity link web: as of icp-cli 1.0.2 it has none (confirm with icp identity link web --help).
Delegation handoff (CLI host and browser on different machines)
icp identity delegation transfers signing authority from an identity linked on the user's machine to a session key on the CLI host, without the browser ever reaching the CLI host. It requires icp installed on the user's machine too, and exists in icp-cli 1.0.x — verify with icp identity delegation --help on both machines. If the user's machine has icp and you can run commands there, the simplest path is to run this whole skill there instead; otherwise:
-
On the CLI host (sandbox) — create a pending identity with a fresh session key:
icp identity delegation request <your-identity-name>
It prints the session public key as a PEM to stdout. Give that PEM block to the user. (If the default --storage keyring fails in a headless sandbox, retry with --storage plaintext.)
-
On the user's machine — the user links there if not already linked (icp identity link web <local-name> --auth <console-origin> — the normal flow works locally), saves the PEM to a file, and signs a delegation to it:
icp identity delegation sign --identity <local-name> --key-pem session-key.pem --duration 8h > delegation.json
--duration takes e.g. 30m, 8h, 1d; optionally --canisters <ids> restricts the delegation. The user sends delegation.json back. If sign fails with delegation for identity <name> has expired or will expire within 5 minutes, the local web session has lapsed — run icp identity reauth <local-name> (the normal browser flow, which works locally) and retry.
-
On the CLI host — complete the pending identity and activate it:
icp identity delegation use <your-identity-name> --from-json delegation.json
icp identity default <your-identity-name>
icp identity principal
Treat delegation.json as a time-limited credential: whoever holds the sandbox's session key can sign as the user's console principal until it expires. Keep the duration short and re-run the handoff when it lapses.
No-network CLI host — hand the user one script
If the CLI shell cannot reach the network, the user's real terminal is the deploy machine — and because their terminal and browser share a loopback there, the normal link flow works; no delegation handoff is needed. Your job shifts to preparation:
-
Get the built project into a directory the user's terminal can reach.
-
Write one script at the project root that does the whole network-bound sequence — adapt this template (fill in the real identity name, console origin, and subnet id; do not leave placeholders):
#!/usr/bin/env bash
set -euo pipefail
icp --version
icp identity list | grep -E '^\*? *<your-identity-name> ' >/dev/null || icp identity link web <your-identity-name> --auth <console-origin>
icp identity default <your-identity-name>
icp deploy -e ic --subnet <subnet-id>
-
Ask the user to run it in their terminal, complete the Internet Identity sign-in when the browser opens, and paste the output back.
-
Verify from the pasted output (canister ids, frontend URL) and continue — a re-deploy for the Step 2 __META_* variables goes through the same script.
Normal link flow (CLI and browser share a loopback)
First check what already exists:
icp identity list
The list does not show which console (if any) an identity was linked against — that cannot be determined from the CLI. Decide like this:
- Only
anonymous (or plain local identities) listed — no web-linked identity exists; run the link command below.
- An identity the user recognizes as their engine identity (by name or principal) — set it active (below) and skip to Step 2.
- Unsure — ask the user, or simply relink under a new name; linking again is cheap and safe.
To link, run this, substituting a name the user picks — <your-identity-name> is any local label, not a fixed value (do not hardcode something like my-engine-admin); reuse the same name in every command below:
icp identity link web <your-identity-name> --auth <console-origin>
-
Use https://opencloud.org as <console-origin> unless the user named a different console. Never omit --auth: the flag has a built-in default (https://id.ai) that is not your console and silently derives the wrong principal.
-
The command first waits at a "Press Enter to log in" prompt before anything happens. Run it interactively when you can; in a non-interactive shell (e.g. a background process) pipe a real newline:
printf '\n' | icp identity link web <your-identity-name> --auth <console-origin>
Do not redirect stdin from /dev/null — the bare EOF does not satisfy the prompt, and the command sits on "Press Enter to log in" indefinitely with no browser ever opening.
-
After Enter it opens a browser tab. The user completes the Internet Identity sign-in there. Wait for them to confirm before continuing — you cannot complete the sign-in for them.
-
--auth must be the exact console origin (scheme + host), e.g. https://opencloud.org. A mismatched origin derives a different principal, and the engine will reject the deploy as unauthorized.
-
This is a one-time, per-machine step.
Then make it the active identity and verify:
icp identity default <your-identity-name>
icp identity default
icp identity principal
Step 2 — Name the app (and give it an icon) in the console (recommended)
By default, CLI-deployed canisters appear on the engine console's Applications page as bare rows labelled only by their principal id. A set of canister environment variables makes the console group them into a single named application with readable per-canister labels, an "Open" button, and an icon. Set them once in your project config:
__META_PROJECT — the application name. Canisters that share the same value are grouped into one named app, so set an identical value on every canister of the app.
__META_NAME — the per-canister display label (e.g. Backend, Frontend).
__META_MAIN_CANISTER — the literal string "true" on exactly one canister (the entry point, usually the frontend/asset canister). This marks the app's main canister: the console reads __META_BASE_URL and __META_ICON_PATH only from it, and the "Open" button targets it.
__META_BASE_URL — an absolute https:// URL, set on the main canister (e.g. the frontend canister's URL https://<frontend-canister-id>.icp.net, or a custom domain). When present and valid, it is the URL the "Open" button opens; when absent or not https, the "Open" button falls back to the main canister's gateway URL. It is also the base that __META_ICON_PATH resolves against.
__META_ICON_PATH — the path to the app icon, resolved against __META_BASE_URL to form the icon the console renders (e.g. /favicon.svg → https://<base>/favicon.svg). Set it on the main canister, alongside __META_BASE_URL.
The icon and "Open" link are read only from the main canister (the one marked __META_MAIN_CANISTER: "true") — __META_BASE_URL / __META_ICON_PATH on any other canister are ignored.
Set them under each canister's settings.environment_variables — this is valid alongside a recipe. With per-canister canister.yaml files:
name: frontend
recipe:
type: "@dfinity/static-site@v0.3.3"
configuration:
build:
- npm install
- npm run build
dir: dist
settings:
environment_variables:
__META_PROJECT: "My App"
__META_NAME: "Frontend"
__META_MAIN_CANISTER: "true"
__META_BASE_URL: "https://<frontend-canister-id>.icp.net"
__META_ICON_PATH: "/favicon.svg"
name: backend
recipe:
type: "@dfinity/motoko@v5.0.0"
settings:
environment_variables:
__META_PROJECT: "My App"
__META_NAME: "Backend"
For a single inline icp.yaml (canisters defined there directly), put the same settings.environment_variables block under each canister entry. Note the inline form: canisters is an array of {name, recipe, settings} items, not a map keyed by canister name:
canisters:
- name: frontend
recipe:
settings:
environment_variables:
__META_PROJECT: "My App"
__META_NAME: "Frontend"
__META_MAIN_CANISTER: "true"
__META_BASE_URL: "https://<frontend-canister-id>.icp.net"
__META_ICON_PATH: "/favicon.svg"
- name: backend
recipe:
settings:
environment_variables:
__META_PROJECT: "My App"
__META_NAME: "Backend"
Notes:
- icp-cli merges these with the
PUBLIC_CANISTER_ID:<name> variables it injects automatically at deploy time — the asset canister keeps serving and the app keeps working. (Verified against icp-cli 0.3.0.)
- All values are strings;
__META_MAIN_CANISTER must be the exact string "true".
- They are applied during
icp deploy (the "Setting environment variables" step). After deploy, confirm with icp canister settings show <name> -e ic.
Icon specifics (the console builds the icon as __META_BASE_URL + __META_ICON_PATH):
- Both must be present and on the main canister for an icon to appear — there is no fallback.
__META_ICON_PATH alone does nothing.
__META_BASE_URL must parse as an absolute https:// URL. A bare host, an http:// URL, or a data: / javascript: value is rejected: the icon then does not render, and the "Open" button falls back to the main canister's gateway URL (it does not disappear). (The console validates the scheme before using it.)
__META_ICON_PATH is a path to an asset your frontend actually serves (e.g. /favicon.svg), not an inline image. The resolved URL is rendered as an <img> src, so it must return an image. Do not put a data: URI here: engine env values are length-capped (≤128 chars observed), so it would not fit, and the field is a path by design.
- The frontend canister's id is only known after the first deploy. The usual flow is: deploy once, read the frontend canister id from the output, set
__META_BASE_URL to https://<that-id>.icp.net (and __META_ICON_PATH), then re-deploy to apply. If you control a custom domain for the app, you can set it up front instead.
Pin the Internet Identity derivation origin (if the app signs users in)
Internet Identity derives a principal per origin. An app reached at both its canister address and a custom domain signs the same person in as two different users.
The engine console names the app's canister address as the origin to derive from. It cannot be renamed or removed, so it survives adding, changing, or dropping a custom domain. It is the address of the canister that serves your frontend (the asset/static-site canister the browser loads the app from), built from that canister's id:
https://<frontend-canister-id>.icp.net
That is usually the canister you marked __META_MAIN_CANISTER: "true", but that flag is a console display setting, not a guarantee. If you marked a backend canister, still derive from the frontend canister. A backend canister cannot be a derivation origin: the browser is never on it, and it cannot serve the .well-known/ii-alternative-origins file Internet Identity fetches from that origin to validate the claim.
Build the value from the canister id, not from __META_BASE_URL. The two match only while __META_BASE_URL still points at the canister address. Pointing it at a custom domain is normal and supported (see the icon notes above), and the two then differ. A custom domain is the one value that must never become the derivation origin.
Do this before the app has users, not after. A domain that has already collected sign-ins cannot be repointed at a derivation origin without orphaning every account created under it. If the app might ever get a custom domain, set derivationOrigin on the first deploy even while the canister address is the only origin — it costs nothing then and cannot be applied retroactively without losing accounts.
For the mechanics — the derivationOrigin option and the .well-known/ii-alternative-origins file, including the _headers entry the @dfinity/static-site recipe needs — load the internet-identity skill.
Caffeine apps are the exception: Caffeine owns their address, so it is not the app's to derive from. The console withholds the canister-address row for them.
Version metadata (recommended)