asset-canister

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Asset 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

前置条件

  • @icp-sdk/canisters
    npm package (for programmatic uploads)
  • @icp-sdk/canisters
    npm package (for programmatic uploads)

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
.icp/data/mappings/
(per environment).
Access patterns:
EnvironmentURL Pattern
Local
http://<canister-id>.localhost:8000
Mainnet
https://<canister-id>.ic0.app
or
https://<canister-id>.icp0.io
Custom domain
https://yourdomain.com
(with DNS configuration)
资产Canister为每个项目单独创建,没有统一的全局Canister ID。部署完成后,你的Canister ID会存储在
.icp/data/mappings/
目录下(按环境区分)。
访问模式:
环境URL格式
本地
http://<canister-id>.localhost:8000
主网
https://<canister-id>.ic0.app
or
https://<canister-id>.icp0.io
自定义域名
https://yourdomain.com
(with DNS configuration)

Mistakes That Break Your Build

导致构建失败的常见错误

  1. Wrong
    source
    path in icp.yaml.
    The
    source
    array must point to the directory containing your build output. If you use Vite, that is
    "dist"
    . If you use Next.js export, it is
    "out"
    . If the path does not exist at deploy time,
    icp deploy
    fails silently or deploys an empty canister.
  2. Missing
    .ic-assets.json5
    for single-page apps.
    Without a rewrite rule, refreshing on
    /about
    returns a 404 because the asset canister looks for a file literally named
    /about
    . You must configure a fallback to
    index.html
    .
  3. Forgetting to build before deploy.
    icp deploy
    runs the
    build
    command from icp.yaml, but if it is empty or misconfigured, the
    source
    directory will be stale or empty.
  4. 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.
  5. Deploying to the wrong canister name. If icp.yaml has
    "frontend"
    but you run
    icp deploy assets
    , it creates a new canister instead of updating the existing one.
  6. 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 in
    @icp-sdk/canisters
    handles 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.
  7. Not configuring
    allow_raw_access
    correctly.
    The asset canister has two serving modes: certified (via
    ic0.app
    /
    icp0.io
    , where HTTP gateways verify response integrity) and raw (via
    raw.ic0.app
    /
    raw.icp0.io
    , where no verification occurs). By default,
    allow_raw_access
    is
    true
    , 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. Set
    "allow_raw_access": false
    in
    .ic-assets.json5
    for any sensitive assets. Only enable raw access when strictly needed.
  1. icp.yaml中的
    source
    路径错误
    source
    数组必须指向包含构建输出的目录。若使用Vite,该目录为
    "dist"
    ;若使用Next.js导出,该目录为
    "out"
    。如果部署时该路径不存在,
    icp deploy
    会静默失败或部署空的Canister。
  2. 单页应用缺少
    .ic-assets.json5
    文件
    。若无重写规则,在
    /about
    页面刷新会返回404,因为资产Canister会查找字面名为
    /about
    的文件。你必须配置回退到
    index.html
  3. 部署前忘记执行构建
    icp deploy
    会运行icp.yaml中的
    build
    命令,但如果该命令为空或配置错误,
    source
    目录的内容会过期或为空。
  4. 未设置content-type请求头。资产Canister会根据文件扩展名推断内容类型。如果程序化上传文件时未设置内容类型,浏览器可能无法正确渲染文件。
  5. 部署到错误的Canister名称。如果icp.yaml中配置的是
    "frontend"
    ,但你执行了
    icp deploy assets
    ,会创建一个新的Canister而非更新现有Canister。
  6. 超出Canister存储限制。资产Canister使用稳定内存,可存储超过4GB的数据。但单个资产受限于2MB的入口消息大小(
    @icp-sdk/canisters
    中的资产管理器会自动对大于1.9MB的文件进行分块上传)。实际需要关注的是存储的总周期成本——大型媒体文件(视频、数据集)会非常昂贵。对于大文件,请使用专用存储解决方案。
  7. allow_raw_access
    配置错误
    。资产Canister有两种服务模式:已认证模式(通过
    ic0.app
    /
    icp0.io
    ,HTTP网关会验证响应完整性)和原始模式(通过
    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.mo
Key fields:
  • recipe.type: "@dfinity/asset-canister@..."
    -- tells
    icp
    this is an asset canister
  • dir
    -- directory to upload (contents, not the directory itself)
  • build
    -- commands
    icp deploy
    runs before uploading (your frontend build step)
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@..."
    —— 告知
    icp
    这是一个资产Canister
  • dir
    —— 要上传的目录(上传目录内容,而非目录本身)
  • build
    ——
    icp deploy
    在上传前执行的命令(你的前端构建步骤)

SPA Routing and Default Headers:
.ic-assets.json5

SPA路由与默认请求头:
.ic-assets.json5

Create this file in your
source
directory (e.g.,
dist/.ic-assets.json5
) or project root. For it to be included in the asset canister, it must end up in the
source
directory at deploy time.
Recommended approach: place the file in your
public/
or
static/
folder so your build tool copies it into
dist/
automatically.
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
"enable_aliasing": true
-- this tells the asset canister to serve
index.html
when a requested path has no matching file.
If the standard security policy above blocks the app from working, overwrite the default security headers with custom values, adding them after
Cache-Control
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
在你的
source
目录(如
dist/.ic-assets.json5
)或项目根目录创建该文件。要使其被包含在资产Canister中,部署时它必须位于
source
目录下。
推荐做法:将该文件放在
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回退生效,关键设置是
"enable_aliasing": true
——这会告知资产Canister,当请求的路径没有匹配文件时,返回
index.html
如果上述标准安全策略导致应用无法运行,请覆盖默认安全请求头,在上面的
Cache-Control
后添加自定义值。请以资深安全工程师的视角,尽可能让这些请求头更安全。标准策略请求头可在此查看:https://github.com/dfinity/sdk/blob/master/src/canisters/frontend/ic-asset/src/security_policy.rs

Content Encoding

内容编码

The asset canister automatically compresses assets with gzip and brotli. No configuration needed. When a browser sends
Accept-Encoding: gzip, br
, the canister serves the compressed version.
To 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压缩资产,无需额外配置。当浏览器发送
Accept-Encoding: gzip, br
请求头时,Canister会返回压缩后的版本。
验证压缩是否生效:
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:
  1. Create a file
    .well-known/ic-domains
    in your
    source
    directory containing your domain:
text
yourdomain.com
www.yourdomain.com
  1. Add DNS records:
text
undefined
要通过自定义域名提供资产Canister服务:
  1. source
    目录创建
    .well-known/ic-domains
    文件,写入你的域名:
text
yourdomain.com
www.yourdomain.com
  1. 添加DNS记录:
text
undefined

CNAME 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 deploy
):
javascript
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 deploy
):
javascript
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
grant_permission
to give principals only the access they need. Do not use
--add-controller
for upload access -- controllers have full canister control (upgrade code, change settings, delete the canister, drain cycles).
bash
undefined
资产Canister内置权限系统,包含三个角色(权限从低到高):
  • Prepare —— 可上传分块并提交批次提案,但无法将其设为生效状态。
  • Commit —— 可上传并提交资产(使其生效)。这是部署流水线的标准角色。
  • ManagePermissions —— 可向其他主体授予或撤销权限。
使用
grant_permission
仅向主体授予所需的权限。请勿使用
--add-controller
获取上传权限——控制器拥有Canister的完全控制权(升级代码、修改设置、删除Canister、消耗周期)。
bash
undefined

Grant "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
undefined
bash
undefined

Start 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
undefined
icp deploy frontend
undefined

Mainnet Deployment

主网部署

bash
undefined
bash
undefined

Ensure you have cycles in your wallet

Ensure you have cycles in your wallet

icp deploy -e ic frontend
undefined
icp deploy -e ic frontend
undefined

Updating Frontend Only

仅更新前端

When you only changed frontend code:
bash
undefined
当仅修改了前端代码时:
bash
undefined

Rebuild and redeploy just the frontend canister

Rebuild and redeploy just the frontend canister

npm run build icp deploy frontend
undefined
npm run build icp deploy frontend
undefined

Verify It Works

验证部署成功

bash
undefined
bash
undefined

1. 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

undefined
undefined