owner key as a plain EOA. The update is a single CLI command that handles the on-chain transactions for you.
If the owner is a Safe, timelock, or DAO contract, the single-command path will not work — jump to Multisig and Governance. For the first-deploy flow and conceptual background, see Deploying with Onchain KMS and Understanding Onchain KMS.
What Triggers a New Compose Hash
Any change that modifiesapp-compose.json produces a new compose_hash and requires a new on-chain addComposeHash call before the CVM can start running the new version. Common triggers:
- Editing
docker-compose.yml(bumping an image tag, adding a service, changing a port, whitespace changes — all count) - Changing
allowed_envs,public_logs,public_sysinfo, orpublic_tcbinfo - Changing the
runnerorpre_launch_script
allowed_envs) do not change the compose hash — they are encrypted and stored separately. But they still go through this same update command.
See What the compose hash is for the full list of fields that participate in hashing.
The Single-Command Update
When the DstackApp owner is still a plain wallet, updating is one command:--cvm-id flag accepts the CVM name (if unique in your workspace), the CVM UUID, or the app_id — whichever is most convenient. The CLI detects that you passed --cvm-id, so it goes down the update path instead of the new-deployment path.
Add -e .env or inline -e KEY=VALUE if you are changing environment variables alongside the compose file. Env var values are encrypted with the CVM’s per-application env-encryption public key before leaving the CLI, same as on first deploy.
Expect two wallet signatures in the worst case (one for addDevice, one for addComposeHash). In the common case where the scheduler kept your CVM on the same node, just one.
What Happens Under the Hood
The single command maps to the following sequence, in order:- Reads the CVM metadata from the backend, including the existing DstackApp contract address and the chain info.
- Patches the CVM with the new compose file (and optional new env vars). The backend computes the new
compose_hashand responds withrequiresOnChainHash: true, along with the device ID of the current node. - Checks on-chain prerequisites by calling a read-only helper that asks the DstackApp contract whether the compose hash and device ID are already allowed.
- If the device is not yet allowed, sends
addDevice(deviceId)from--private-key. This happens automatically when the scheduler puts your CVM on a node it has not used before. - Sends
addComposeHash(newHash)from--private-key. This is the main update transaction. It is idempotent at the contract level — the CLI sends it even if a state read says the hash is already allowed, so the backend can confirm against a real transaction hash. - Confirms the patch with the backend. The backend reads the receipt, checks the
ComposeHashAddedevent, re-reads the contract state, and rolls the CVM over to the new compose file.
GET /api/v1/cvms/<id>— read existing CVM metadata.POST /api/v1/cvms/<id>(patch) — send new compose file and encrypted env. Backend returns newcomposeHash,deviceId, andrequiresOnChainHash: true.- Read-only chain call —
DstackApp.allowedDeviceIds(deviceId)andDstackApp.allowedComposeHashes(composeHash)batched through a single RPC. - If needed, on-chain
DstackApp.addDevice(deviceId)signed by--private-key. - On-chain
DstackApp.addComposeHash(composeHash)signed by--private-key. The CLI waits for one confirmation. POST /api/v1/cvms/<id>/confirm-patch— the backend verifies the receipt, re-reads the contract state, and rolls the CVM onto the new compose.
What Changes and What Does Not
When you run an update like this, the following stay the same:- The DstackApp contract address. It is still your
app_id. Clients already talking to your service do not need to change anything. - The CVM’s
vm_uuid. The dashboard URL for the CVM does not change. - The KMS-derived application keys. Key derivation is tied to the
app_id, not to the compose hash, so your persistent encrypted storage continues to decrypt correctly across updates.
compose_hash— a new hash is now inallowedComposeHashes.- The container image and env var ciphertext. The CLI re-encrypts new env values for the CVM’s env-encryption key.
- The running processes inside the CVM. The old compose is replaced by the new one.
removeComposeHash(oldHash) on the DstackApp from your wallet after the update is live.
When This Simple Flow Does Not Work
The single-command EOA path assumes--private-key can sign transactions that the DstackApp will accept. Two situations break that assumption:
- The owner is no longer an EOA. If you transferred ownership to a Safe, a timelock, or any other contract,
addComposeHashcalled from a bare wallet will revert on-chain. You need the prepare-and-commit flow so the transaction can be routed through your governance contract. See Multisig and Governance. - You want to add or remove a node without changing the compose file. Device allowlisting is a separate flow from compose-hash updates. See Device Management.
Troubleshooting
'Transaction verification failed on backend' during the update
'Transaction verification failed on backend' during the update
The backend reads the receipt for the
addComposeHash transaction and looks for the ComposeHashAdded(bytes32) event. This can fail briefly right after sending due to RPC propagation delay. The backend retries a few times with short gaps before giving up. If you consistently see this error:- Double-check that
--rpc-urlpoints to a healthy endpoint and is consistent between your signing RPC and the backend’s read RPC. - Verify the transaction actually succeeded on the block explorer — a reverted transaction will also produce this error.
- If everything looks fine on-chain but the backend still rejects, retry the update from the start. The CLI will re-send
addComposeHash, which is idempotent at the contract level.
'Owner mismatch' or EVM revert on addComposeHash
'Owner mismatch' or EVM revert on addComposeHash
The wallet behind
--private-key is not the current DstackApp owner. Open the DstackApp contract on Basescan or Etherscan and read owner():- If
owner()returns a different EOA, you are signing with the wrong key. Switch keys and retry. - If
owner()returns a contract address (Safe, Timelock, or other), the EOA path will never work. Switch to Multisig and Governance.
Compose hash did not change when I expected it to
Compose hash did not change when I expected it to
You edited something that does not affect
app-compose.json. Env var values, the --private-key, and the --rpc-url do not participate in the hash. Only the fields listed in What the compose hash is do.Run the update anyway — if the CLI sees no hash change, it skips addComposeHash entirely and only pushes the new env var ciphertext through the patch endpoint.CVM stuck in 'updating' or 'pending' after confirm-patch succeeded
CVM stuck in 'updating' or 'pending' after confirm-patch succeeded
Commit succeeded but the CVM restart is still rolling. Check the CVM logs with
phala logs <cvm-name-or-id> or in the dashboard. The common slow paths are image pull time (large images on a fresh node) and the workload’s own startup time. If the CVM keeps reverting to the old compose, open a support ticket with the correlation_id from the CLI output.The CLI says 'Chain required for publicClient' or 'Chain required for walletClient'
The CLI says 'Chain required for publicClient' or 'Chain required for walletClient'
The CLI could not resolve the chain from the CVM metadata and no
--rpc-url / ETH_RPC_URL fallback was provided. Set --rpc-url explicitly and retry, or set ETH_RPC_URL in your environment. You can verify with echo $ETH_RPC_URL.Error codes
Errors from the confirm step map to structured error codes from Module 01 of the Error Codes reference. The four that matter for this flow areERR-01-005 through ERR-01-008. Each one tells you something specific:
ERR-01-005(HTTP 465) — the compose hash is not yet onallowedComposeHashes. Usually a transient state during the confirm step if you bailed out beforeaddComposeHashlanded; retry.ERR-01-006(HTTP 466) — the provision cache for this compose hash has expired (14-day TTL) or the compose file changed since you started the update. Re-run the update command.ERR-01-007(HTTP 467) — the transaction hash you (or the CLI) passed does not verify against the expectedaddComposeHashstate change. Usually RPC propagation delay or a reverted tx; see the accordion above.ERR-01-008(HTTP 468) — the compose hash is genuinely not inallowedComposeHashesfrom the contract’s point of view. If this appears afteraddComposeHashsupposedly succeeded, you probably called it on the wrong contract — check the DstackApp address on Basescan.
Next Steps
- Multisig and Governance — the prepare-and-commit flow for compose updates when the DstackApp owner is a Safe, a timelock, a DAO, or any custom contract. Also covers webhook notifications for pending approvals.
- Device Management — how to add and remove nodes from the allowlist without touching the compose file, and how to recover from hardware-driven
device_iddrift. - Understanding Onchain KMS — the conceptual model behind compose hashes and the boot-time verification flow.
- Error Codes reference — the full catalog of
ERR-01-*codes returned by the CVM API.

