asset-canister
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAsset Canister & Frontend Hosting
资产Canister与前端托管
What This Is
简介
The asset canister hosts static files (HTML, CSS, JS, images) directly on the Internet Computer. This is how web frontends are deployed on-chain. Responses are certified by the subnet, and HTTP gateways automatically verify integrity, i.e. that content was served by the blockchain. The content can also be verified in the browser -- not a centralized server.
资产Canister可在Internet Computer(IC)网络上直接托管静态文件(HTML、CSS、JS、图片),这是Web前端上链部署的方式。响应由子网认证,HTTP网关会自动验证完整性,即确保内容由区块链提供。内容也可在浏览器中验证——而非通过中心化服务器。
Prerequisites
前置条件
- npm package (for programmatic uploads)
@icp-sdk/canisters
- npm package (for programmatic uploads)
@icp-sdk/canisters
Canister IDs
Canister ID
Asset canisters are created per-project. There is no single global canister ID. After deployment, your canister ID is stored in (per environment).
.icp/data/mappings/Access patterns:
| Environment | URL Pattern |
|---|---|
| Local | |
| Mainnet | |
| Custom domain | |
资产Canister为每个项目单独创建,没有统一的全局Canister ID。部署完成后,你的Canister ID会存储在目录下(按环境区分)。
.icp/data/mappings/访问模式:
| 环境 | URL格式 |
|---|---|
| 本地 | |
| 主网 | |
| 自定义域名 | |
Mistakes That Break Your Build
导致构建失败的常见错误
-
Wrongpath in icp.yaml. The
sourcearray must point to the directory containing your build output. If you use Vite, that issource. If you use Next.js export, it is"dist". If the path does not exist at deploy time,"out"fails silently or deploys an empty canister.icp deploy -
Missingfor single-page apps. Without a rewrite rule, refreshing on
.ic-assets.json5returns a 404 because the asset canister looks for a file literally named/about. You must configure a fallback to/about.index.html -
Forgetting to build before deploy.runs the
icp deploycommand from icp.yaml, but if it is empty or misconfigured, thebuilddirectory will be stale or empty.source -
Not setting content-type headers. The asset canister infers content types from file extensions. If you upload files programmatically without setting the content type, browsers may not render them correctly.
-
Deploying to the wrong canister name. If icp.yaml hasbut you run
"frontend", it creates a new canister instead of updating the existing one.icp deploy assets -
Exceeding canister storage limits. The asset canister uses stable memory, which can hold well over 4GB. However, individual assets are limited by the 2MB ingress message size (the asset manager inhandles chunking automatically for uploads >1.9MB). The practical concern is total cycle cost for storage -- large media files (videos, datasets) become expensive. Use a dedicated storage solution for large files.
@icp-sdk/canisters -
Not configuringcorrectly. The asset canister has two serving modes: certified (via
allow_raw_access/ic0.app, where HTTP gateways verify response integrity) and raw (viaicp0.io/raw.ic0.app, where no verification occurs). By default,raw.icp0.ioisallow_raw_access, meaning assets are also available on the raw domain. On the raw domain, boundary nodes or a network-level attacker can tamper with response content undetected. Settruein"allow_raw_access": falsefor any sensitive assets. Only enable raw access when strictly needed..ic-assets.json5
-
icp.yaml中的路径错误。
source数组必须指向包含构建输出的目录。若使用Vite,该目录为source;若使用Next.js导出,该目录为"dist"。如果部署时该路径不存在,"out"会静默失败或部署空的Canister。icp deploy -
单页应用缺少文件。若无重写规则,在
.ic-assets.json5页面刷新会返回404,因为资产Canister会查找字面名为/about的文件。你必须配置回退到/about。index.html -
部署前忘记执行构建。会运行icp.yaml中的
icp deploy命令,但如果该命令为空或配置错误,build目录的内容会过期或为空。source -
未设置content-type请求头。资产Canister会根据文件扩展名推断内容类型。如果程序化上传文件时未设置内容类型,浏览器可能无法正确渲染文件。
-
部署到错误的Canister名称。如果icp.yaml中配置的是,但你执行了
"frontend",会创建一个新的Canister而非更新现有Canister。icp deploy assets -
超出Canister存储限制。资产Canister使用稳定内存,可存储超过4GB的数据。但单个资产受限于2MB的入口消息大小(中的资产管理器会自动对大于1.9MB的文件进行分块上传)。实际需要关注的是存储的总周期成本——大型媒体文件(视频、数据集)会非常昂贵。对于大文件,请使用专用存储解决方案。
@icp-sdk/canisters -
配置错误。资产Canister有两种服务模式:已认证模式(通过
allow_raw_access/ic0.app,HTTP网关会验证响应完整性)和原始模式(通过icp0.io/raw.ic0.app,无验证机制)。默认情况下raw.icp0.io为allow_raw_access,意味着资产也可在原始域名访问。在原始域名下,边界节点或网络层面的攻击者可篡改响应内容而不被发现。对于敏感资产,需在true中设置.ic-assets.json5。仅在必要时启用原始访问。"allow_raw_access": false
Implementation
实现步骤
icp.yaml Configuration
icp.yaml配置
yaml
canisters:
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.1.0"
configuration:
dir: dist
build:
- npm install
- npm run build
- name: backend
recipe:
type: "@dfinity/motoko@v4.1.0"
configuration:
main: src/backend/main.moKey fields:
- -- tells
recipe.type: "@dfinity/asset-canister@..."this is an asset canistericp - -- directory to upload (contents, not the directory itself)
dir - -- commands
buildruns before uploading (your frontend build step)icp deploy
yaml
canisters:
- name: frontend
recipe:
type: "@dfinity/asset-canister@v2.1.0"
configuration:
dir: dist
build:
- npm install
- npm run build
- name: backend
recipe:
type: "@dfinity/motoko@v4.1.0"
configuration:
main: src/backend/main.mo关键字段:
- —— 告知
recipe.type: "@dfinity/asset-canister@..."这是一个资产Canistericp - —— 要上传的目录(上传目录内容,而非目录本身)
dir - ——
build在上传前执行的命令(你的前端构建步骤)icp deploy
SPA Routing and Default Headers: .ic-assets.json5
.ic-assets.json5SPA路由与默认请求头:.ic-assets.json5
.ic-assets.json5Create this file in your directory (e.g., ) or project root. For it to be included in the asset canister, it must end up in the directory at deploy time.
sourcedist/.ic-assets.json5sourceRecommended approach: place the file in your or folder so your build tool copies it into automatically.
public/static/dist/json5
[
{
// Default headers for all paths: caching, security, and raw access policy
"match": "**/*",
"security_policy": "standard",
"headers": {
"Cache-Control": "public, max-age=0, must-revalidate"
},
// Disable raw (uncertified) access by default -- see mistake #7 above
"allow_raw_access": false
},
{
// Cache static assets aggressively (they have content hashes in filenames)
"match": "assets/**/*",
"headers": {
"Cache-Control": "public, max-age=31536000, immutable"
}
},
{
// SPA fallback: serve index.html for any unmatched route
"match": "**/*",
"enable_aliasing": true
}
]For the SPA fallback to work, the critical setting is -- this tells the asset canister to serve when a requested path has no matching file.
"enable_aliasing": trueindex.htmlIf the standard security policy above blocks the app from working, overwrite the default security headers with custom values, adding them after above. Act like a senior security engineer, making these headers as secure as possible. The standard policy headers can be found here: https://github.com/dfinity/sdk/blob/master/src/canisters/frontend/ic-asset/src/security_policy.rs
Cache-Control在你的目录(如)或项目根目录创建该文件。要使其被包含在资产Canister中,部署时它必须位于目录下。
sourcedist/.ic-assets.json5source推荐做法:将该文件放在或文件夹中,这样构建工具会自动将其复制到目录。
public/static/dist/json5
[
{
// Default headers for all paths: caching, security, and raw access policy
"match": "**/*",
"security_policy": "standard",
"headers": {
"Cache-Control": "public, max-age=0, must-revalidate"
},
// Disable raw (uncertified) access by default -- see mistake #7 above
"allow_raw_access": false
},
{
// Cache static assets aggressively (they have content hashes in filenames)
"match": "assets/**/*",
"headers": {
"Cache-Control": "public, max-age=31536000, immutable"
}
},
{
// SPA fallback: serve index.html for any unmatched route
"match": "**/*",
"enable_aliasing": true
}
]要使SPA回退生效,关键设置是——这会告知资产Canister,当请求的路径没有匹配文件时,返回。
"enable_aliasing": trueindex.html如果上述标准安全策略导致应用无法运行,请覆盖默认安全请求头,在上面的后添加自定义值。请以资深安全工程师的视角,尽可能让这些请求头更安全。标准策略请求头可在此查看:https://github.com/dfinity/sdk/blob/master/src/canisters/frontend/ic-asset/src/security_policy.rs
Cache-ControlContent Encoding
内容编码
The asset canister automatically compresses assets with gzip and brotli. No configuration needed. When a browser sends , the canister serves the compressed version.
Accept-Encoding: gzip, brTo verify compression is working:
bash
icp canister call frontend http_request '(record {
url = "/";
method = "GET";
body = vec {};
headers = vec { record { "Accept-Encoding"; "gzip" } };
certificate_version = opt 2;
})'资产Canister会自动使用gzip和brotli压缩资产,无需额外配置。当浏览器发送请求头时,Canister会返回压缩后的版本。
Accept-Encoding: gzip, br验证压缩是否生效:
bash
icp canister call frontend http_request '(record {
url = "/";
method = "GET";
body = vec {};
headers = vec { record { "Accept-Encoding"; "gzip" } };
certificate_version = opt 2;
})'Custom Domain Setup
自定义域名设置
To serve your asset canister from a custom domain:
- Create a file in your
.well-known/ic-domainsdirectory containing your domain:source
text
yourdomain.com
www.yourdomain.com- Add DNS records:
text
undefined要通过自定义域名提供资产Canister服务:
- 在目录创建
source文件,写入你的域名:.well-known/ic-domains
text
yourdomain.com
www.yourdomain.com- 添加DNS记录:
text
undefinedCNAME record pointing to boundary nodes
CNAME record pointing to boundary nodes
yourdomain.com. CNAME icp1.io.
yourdomain.com. CNAME icp1.io.
ACME challenge record for TLS certificate provisioning
ACME challenge record for TLS certificate provisioning
_acme-challenge.yourdomain.com. CNAME _acme-challenge.<your-canister-id>.icp2.io.
_acme-challenge.yourdomain.com. CNAME _acme-challenge.<your-canister-id>.icp2.io.
Canister ID TXT record for verification
Canister ID TXT record for verification
_canister-id.yourdomain.com. TXT "<your-canister-id>"
3. Deploy your canister so the `.well-known/ic-domains` file is available, then register the custom domain with the boundary nodes. Registration is automatic -- the boundary nodes periodically check for the `.well-known/ic-domains` file and the DNS records. No NNS proposal is needed.
4. Wait for the boundary nodes to pick up the registration and provision the TLS certificate. This typically takes a few minutes. You can verify by visiting `https://yourdomain.com` once DNS has propagated._canister-id.yourdomain.com. TXT "<your-canister-id>"
3. 部署Canister以确保`.well-known/ic-domains`文件可用,然后向边界节点注册自定义域名。注册是自动的——边界节点会定期检查`.well-known/ic-domains`文件和DNS记录,无需提交NNS提案。
4. 等待边界节点完成注册并颁发TLS证书,这通常需要几分钟。DNS生效后,你可通过访问`https://yourdomain.com`进行验证。Programmatic Uploads with @icp-sdk/canisters
使用@icp-sdk/canisters进行程序化上传
For uploading files from code (not just via ):
icp deployjavascript
import { AssetManager } from "@icp-sdk/canisters/assets"; // Asset management utility
import { HttpAgent } from "@icp-sdk/core/agent";
// SECURITY: shouldFetchRootKey fetches the root public key from the replica at
// runtime. In production the root key is hardcoded and trusted. Fetching it at
// runtime lets a man-in-the-middle supply a fake key and forge certified responses.
// NEVER set shouldFetchRootKey to true when host points to mainnet.
const LOCAL_REPLICA = "http://localhost:8000";
const MAINNET = "https://ic0.app";
const host = LOCAL_REPLICA; // Change to MAINNET for production
const agent = await HttpAgent.create({
host,
// Only fetch the root key when talking to a local replica.
// Setting this to true against mainnet is a security vulnerability.
shouldFetchRootKey: host === LOCAL_REPLICA,
});
const assetManager = new AssetManager({
canisterId: "your-asset-canister-id",
agent,
});
// Upload a single file
// Files >1.9MB are automatically chunked (16 parallel chunks)
const key = await assetManager.store(fileBuffer, {
fileName: "photo.jpg",
contentType: "image/jpeg",
path: "/uploads",
});
console.log("Uploaded to:", key); // "/uploads/photo.jpg"
// List all assets
const assets = await assetManager.list();
console.log(assets); // [{ key: "/index.html", content_type: "text/html", ... }, ...]
// Delete an asset
await assetManager.delete("/uploads/old-photo.jpg");
// Batch upload a directory
import { readFileSync, readdirSync } from "fs";
const files = readdirSync("./dist");
for (const file of files) {
const content = readFileSync(`./dist/${file}`);
await assetManager.store(content, { fileName: file, path: "/" });
}通过代码上传文件(而非仅使用):
icp deployjavascript
import { AssetManager } from "@icp-sdk/canisters/assets"; // Asset management utility
import { HttpAgent } from "@icp-sdk/core/agent";
// SECURITY: shouldFetchRootKey fetches the root public key from the replica at
// runtime. In production the root key is hardcoded and trusted. Fetching it at
// runtime lets a man-in-the-middle supply a fake key and forge certified responses.
// NEVER set shouldFetchRootKey to true when host points to mainnet.
const LOCAL_REPLICA = "http://localhost:8000";
const MAINNET = "https://ic0.app";
const host = LOCAL_REPLICA; // Change to MAINNET for production
const agent = await HttpAgent.create({
host,
// Only fetch the root key when talking to a local replica.
// Setting this to true against mainnet is a security vulnerability.
shouldFetchRootKey: host === LOCAL_REPLICA,
});
const assetManager = new AssetManager({
canisterId: "your-asset-canister-id",
agent,
});
// Upload a single file
// Files >1.9MB are automatically chunked (16 parallel chunks)
const key = await assetManager.store(fileBuffer, {
fileName: "photo.jpg",
contentType: "image/jpeg",
path: "/uploads",
});
console.log("Uploaded to:", key); // "/uploads/photo.jpg"
// List all assets
const assets = await assetManager.list();
console.log(assets); // [{ key: "/index.html", content_type: "text/html", ... }, ...]
// Delete an asset
await assetManager.delete("/uploads/old-photo.jpg");
// Batch upload a directory
import { readFileSync, readdirSync } from "fs";
const files = readdirSync("./dist");
for (const file of files) {
const content = readFileSync(`./dist/${file}`);
await assetManager.store(content, { fileName: file, path: "/" });
}Authorization for Uploads
上传授权
The asset canister has a built-in permission system with three roles (from least to most privileged):
- Prepare -- can upload chunks and propose batches, but cannot commit them live.
- Commit -- can upload and commit assets (make them live). This is the standard role for deploy pipelines.
- ManagePermissions -- can grant and revoke permissions to other principals.
Use to give principals only the access they need. Do not use for upload access -- controllers have full canister control (upgrade code, change settings, delete the canister, drain cycles).
grant_permission--add-controllerbash
undefined资产Canister内置权限系统,包含三个角色(权限从低到高):
- Prepare —— 可上传分块并提交批次提案,但无法将其设为生效状态。
- Commit —— 可上传并提交资产(使其生效)。这是部署流水线的标准角色。
- ManagePermissions —— 可向其他主体授予或撤销权限。
使用仅向主体授予所需的权限。请勿使用获取上传权限——控制器拥有Canister的完全控制权(升级代码、修改设置、删除Canister、消耗周期)。
grant_permission--add-controllerbash
undefinedGrant "prepare" permission (can upload but not commit) -- use for preview/staging workflows
Grant "prepare" permission (can upload but not commit) -- use for preview/staging workflows
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { Prepare } })'
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { Prepare } })'
Grant commit permission -- use for deploy pipelines that need to publish assets
Grant commit permission -- use for deploy pipelines that need to publish assets
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { Commit } })'
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { Commit } })'
Grant permission management -- use for principals that need to onboard/offboard other uploaders
Grant permission management -- use for principals that need to onboard/offboard other uploaders
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { ManagePermissions } })'
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { ManagePermissions } })'
List current permissions
List current permissions
icp canister call frontend list_permitted '(record { permission = variant { Commit } })'
icp canister call frontend list_permitted '(record { permission = variant { Commit } })'
Revoke a permission
Revoke a permission
icp canister call frontend revoke_permission '(record { of_principal = principal "<principal-id>"; permission = variant { Commit } })'
> **Security Warning:** `icp canister update-settings frontend --add-controller <principal-id>` grants full canister control -- not just upload permission. A controller can upgrade the canister WASM, change all settings, or delete the canister entirely. Only add controllers when you genuinely need full administrative access.icp canister call frontend revoke_permission '(record { of_principal = principal "<principal-id>"; permission = variant { Commit } })'
> **安全警告:** `icp canister update-settings frontend --add-controller <principal-id>`会授予完全的Canister控制权——而非仅上传权限。控制器可升级Canister WASM、修改所有设置或完全删除Canister。仅在确实需要完整管理权限时添加控制器。Deploy & Test
部署与测试
Local Deployment
本地部署
bash
undefinedbash
undefinedStart the local network
Start the local network
icp network start -d
icp network start -d
Build and deploy frontend + backend
Build and deploy frontend + backend
icp deploy
icp deploy
Or deploy only the frontend
Or deploy only the frontend
icp deploy frontend
undefinedicp deploy frontend
undefinedMainnet Deployment
主网部署
bash
undefinedbash
undefinedEnsure you have cycles in your wallet
Ensure you have cycles in your wallet
icp deploy -e ic frontend
undefinedicp deploy -e ic frontend
undefinedUpdating Frontend Only
仅更新前端
When you only changed frontend code:
bash
undefined当仅修改了前端代码时:
bash
undefinedRebuild and redeploy just the frontend canister
Rebuild and redeploy just the frontend canister
npm run build
icp deploy frontend
undefinednpm run build
icp deploy frontend
undefinedVerify It Works
验证部署成功
bash
undefinedbash
undefined1. Check the canister is running
1. 检查Canister是否运行
icp canister status frontend
icp canister status frontend
Expected: Status: Running, Memory Size: <non-zero>
Expected: Status: Running, Memory Size: <non-zero>
2. List uploaded assets
2. 列出已上传资产
icp canister call frontend list '(record {})'
icp canister call frontend list '(record {})'
Expected: A list of asset keys like "/index.html", "/assets/index-abc123.js", etc.
Expected: A list of asset keys like "/index.html", "/assets/index-abc123.js", etc.
3. Fetch the index page via http_request
3. 通过http_request获取首页
icp canister call frontend http_request '(record {
url = "/";
method = "GET";
body = vec {};
headers = vec {};
certificate_version = opt 2;
})'
icp canister call frontend http_request '(record {
url = "/";
method = "GET";
body = vec {};
headers = vec {};
certificate_version = opt 2;
})'
Expected: record { status_code = 200; body = blob "<!DOCTYPE html>..."; ... }
Expected: record { status_code = 200; body = blob "<!DOCTYPE html>..."; ... }
4. Test SPA fallback (should return index.html, not 404)
4. 测试SPA回退(应返回index.html,而非404)
icp canister call frontend http_request '(record {
url = "/about";
method = "GET";
body = vec {};
headers = vec {};
certificate_version = opt 2;
})'
icp canister call frontend http_request '(record {
url = "/about";
method = "GET";
body = vec {};
headers = vec {};
certificate_version = opt 2;
})'
Expected: status_code = 200 (same content as "/"), NOT 404
Expected: status_code = 200 (same content as "/"), NOT 404
5. Open in browser
5. 在浏览器中打开
Local: http://<frontend-canister-id>.localhost:8000
Local: http://<frontend-canister-id>.localhost:8000
Mainnet: https://<frontend-canister-id>.ic0.app
Mainnet: https://<frontend-canister-id>.ic0.app
6. Get canister ID
6. 获取Canister ID
icp canister id frontend
icp canister id frontend
Expected: prints the canister ID (e.g., "bkyz2-fmaaa-aaaaa-qaaaq-cai")
Expected: prints the canister ID (e.g., "bkyz2-fmaaa-aaaaa-qaaaq-cai")
7. Check storage usage
7. 检查存储使用情况
icp canister info frontend
icp canister info frontend
Shows memory usage, module hash, controllers
Shows memory usage, module hash, controllers
undefinedundefined