关于 custom-domains
这是一项指导开发者将 Internet Computer canister 部署到自定义域名的技能,涵盖 DNS 配置、所有权验证、注册、TLS 证书配置及域名更新。
主要功能
- 01配置 DNS 记录
- 02部署域名所有权文件
- 03验证并注册域名
- 04检查证书和注册状态
- 05更新或移除域名
适用场景
- 01通过品牌域名发布 canister
- 02排查自定义域名注册失败
- 03将域名切换到其他 canister
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dfinity/ icskills --skill custom-domains安装后在 Claude Code 中输入/custom-domains确认已加载。
详细介绍
自定义域名
功能介绍
默认情况下,金丝雀可通过 <canister-id>.icp.net 访问。自定义域名服务可让你通过自己的域名(例如 yourdomain.com)提供任意金丝雀的服务。你需要配置 DNS、将域名所有权文件部署到金丝雀,并通过 REST API 注册。之后,HTTP 网关会自动处理 TLS 证书的签发、续期和路由。
自定义域名在边界节点层级生效——它们通过 DNS 将域名映射到任意金丝雀 ID。任何能通过 HTTP 提供 /.well-known/ic-domains 的金丝雀都可以使用此功能,不仅限于资产金丝雀。包括资产金丝雀、Juno 卫星和实现了 http_request 的自定义金丝雀。
前提条件
- 在任意域名注册商处注册的域名(例如 Namecheap、GoDaddy、Cloudflare)
- 有权限编辑该域名的 DNS 记录
- 已部署且能通过 HTTP 提供
/.well-known/ic-domains的金丝雀(资产金丝雀、Juno 卫星或任何实现了http_request的金丝雀) - 用于调用注册 API 的
curl jq(可选,用于格式化 JSON 响应)
可能导致配置失败的错误
-
未关闭 DNS 提供商的 SSL/TLS。 Cloudflare 等提供商默认启用 Universal SSL。这会干扰 IC 用于签发证书的 ACME 挑战流程,并可能导致证书无法续期。注册前,请关闭 DNS 提供商提供的所有证书/SSL/TLS 服务。
-
在顶级域名上设置 CNAME。 许多 DNS 提供商不允许在顶级域名上设置 CNAME 记录(例如,不带子域名的
example.com)。如果提供商支持,请使用 ANAME 或 ALIAS 记录类型(CNAME 扁平化)。否则,请使用www.example.com这类子域名。 -
缺少
_acme-challengeCNAME。 如果没有将_acme-challenge.CUSTOM_DOMAIN指向_acme-challenge.CUSTOM_DOMAIN.icp2.io,HTTP 网关就无法获取 TLS 证书,注册也会失败。 -
_canister-id上存在多条 TXT 记录。 如果_canister-id.CUSTOM_DOMAIN存在多条 TXT 记录,注册就会失败。请确保只保留一条包含你的金丝雀 ID 的记录。 -
忘记
.well-known/ic-domains文件。 金丝雀必须提供列有自定义域名的/.well-known/ic-domains。如果缺少此文件,注册期间的域名所有权验证就会失败。 -
DNS 提供商留下了过期的
_acme-challengeTXT 记录。 提供商之前发起的 ACME 挑战可能会在_acme-challenge.CUSTOM_DOMAIN上留下 TXT 记录,这些记录不会显示在控制面板中。它们会与 IC 的 ACME 流程冲突。请关闭提供商提供的所有 TLS 服务以清除这些记录。使用dig TXT _acme-challenge.CUSTOM_DOMAIN验证。 -
未明确注册域名。 仅配置 DNS 并不足够。你必须调用
POST /custom-domains/v1/CUSTOM_DOMAIN来启动注册。注册不会自动进行。 -
将
HttpAgent的host设置为自定义域名。host是金丝雀调用所使用的 API 端点,不是前端的访问域名。自定义域名是 HTTP 网关,不提供/api/v2,因此将host指向该域名(或window.location.origin)会导致调用失败。你无需设置host:在自定义域名上,较新版本的@icp-sdk/coreHttpAgent会在省略host时将其解析为https://icp-api.io(主网 API 边界节点)。请将其留空,或明确设置为https://icp-api.io——切勿设置为网关域名。 -
为已有用户的应用添加自定义域名。 II 主体按来源区分,因此新域名会为每位回访用户创建全新账户。请固定派生来源——金丝雀地址,自定义域名无法取代它——并在域名上线 之前 将域名列入
.well-known/ii-alternative-origins。域名一旦已有用户登录,就无法在不导致这些账户失联的情况下重新指向其他地址。请参阅internet-identity技能。
实现
步骤 1:配置 DNS 记录
添加三条 DNS 记录(将 CUSTOM_DOMAIN 替换为你的域名,例如 app.example.com):
| 记录类型 | 主机 | 值 |
|---|---|---|
| CNAME | CUSTOM_DOMAIN | CUSTOM_DOMAIN.icp1.io |
| TXT | _canister-id.CUSTOM_DOMAIN | 你的 canister ID(例如 hwvjt-wqaaa-aaaam-qadra-cai) |
| CNAME | _acme-challenge.CUSTOM_DOMAIN | _acme-challenge.CUSTOM_DOMAIN.icp2.io |
有些 DNS 提供商会省略主域名后缀。对于这类提供商上的 app.example.com:
- 使用
app,而不是app.example.com - 使用
_canister-id.app,而不是_canister-id.app.example.com - 使用
_acme-challenge.app,而不是_acme-challenge.app.example.com
对于不支持 CNAME 的顶级域名,请使用提供商的 ANAME 或 ALIAS 记录类型,并将其指向 CUSTOM_DOMAIN.icp1.io。
步骤 2:创建 ic-domains 文件
你的 canister 必须通过 HTTP 提供 /.well-known/ic-domains。创建此文件,每行列出一个自定义域名:
app.example.com
www.example.com
前端 canister 用户: 将 .well-known/ 放在 public/ 目录(Vite 项目)中,或放在源文件旁边,确保它最终进入 dir。使用推荐的 @dfinity/static-site recipe 时,.well-known/ 会自动上传(无需配置)。使用旧版 @dfinity/asset-canister recipe 时,请确保 .ic-assets.json5 包含 { "match": ".well-known", "ignore": false },以便部署这个隐藏目录。有关文件放置位置的详情,请参阅 static-site 技能。
自定义 http_request canister: 在 HTTP 请求处理程序中直接提供 /.well-known/ic-domains 的文件内容。
步骤 3:部署
部署你的 canister,确保可通过 https://<canister-id>.icp.net/.well-known/ic-domains 访问 /.well-known/ic-domains。
步骤 4:验证
注册前检查 DNS 记录和 canister 配置:
curl -sL -X GET "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN/validate" | jq
成功响应:
{
"status": "success",
"message": "Domain is eligible for registration: DNS records are valid and canister ownership is verified",
"data": {
"domain": "CUSTOM_DOMAIN",
"canister_id": "CANISTER_ID",
"validation_status": "valid"
}
}
如果验证失败,常见错误及修复方法如下:
| 错误 | 修复方法 |
|---|---|
| 缺少 DNS CNAME 记录 | 添加 _acme-challenge CNAME,并将其指向 _acme-challenge.CUSTOM_DOMAIN.icp2.io |
| 缺少 DNS TXT 记录 | 添加 _canister-id TXT 记录,并填入你的 canister ID |
| DNS TXT 记录无效 | 确保 TXT 值是有效的 canister ID |
| DNS TXT 记录多于一条 | 删除重复的 _canister-id TXT 记录,只保留一条 |
| 无法获取已知域名 | 确保已部署 /.well-known/ic-domains,且 canister 会提供该文件 |
| 域名不在列表中 | 将域名添加到 ic-domains 文件并重新部署 |
步骤 5:注册
curl -sL -X POST "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq
成功响应:
{
"status": "success",
"message": "Domain registration request accepted and may take a few minutes to process",
"data": {
"domain": "CUSTOM_DOMAIN",
"canister_id": "CANISTER_ID"
}
}
步骤 6:等待证书签发
轮询,直到 registration_status 为 registered:
curl -sL -X GET "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq
状态值:registering → registered(成功),或 failed(检查错误消息)。
状态变为 registered 后,再等待几分钟,让变更传播到所有 HTTP 网关,然后再进行测试。
更新自定义域名
要将现有自定义域名指向其他 canister:
- 将
_canister-idTXT 记录更新为新的 canister ID。 - 通知服务:
curl -sL -X PATCH "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq
- 检查状态:
curl -sL -X GET "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq
移除自定义域名
- 从 DNS 中移除
_canister-idTXT 记录和_acme-challengeCNAME。 - 通知服务:
curl -sL -X DELETE "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq
- 确认已删除(应返回 404):
curl -sL -X GET "https://icp.net/custom-domains/v1/CUSTOM_DOMAIN" | jq
HttpAgent 配置
通过自定义域名提供的前端仍会通过主网 API 边界节点(https://icp-api.io)调用 canister,而不是通过其自身提供服务的域名调用。HttpAgent 的 host 是该 API 端点,而不是 前端的来源。自定义域名是 HTTP 网关,不提供 /api/v2。
你无需设置 host。省略该参数时,较新的 @icp-sdk/core HttpAgent 在自定义域名(以及 icp.net)上会解析到 https://icp-api.io。不要将 host 指向你的自定义域名或 window.location.origin——那是网关,因此调用会失败。
import { HttpAgent } from "@icp-sdk/core/agent";
// host omitted on a custom domain → resolves to https://icp-api.io
const agent = await HttpAgent.create();
// equivalent, explicit:
const agentExplicit = await HttpAgent.create({ host: "https://icp-api.io" });
部署与测试
# 1. Deploy your canister with the ic-domains file served at /.well-known/ic-domains
# 2. Validate DNS + canister config
curl -sL -X GET "https://icp.net/custom-domains/v1/yourdomain.com/validate" | jq
# 3. Register
curl -sL -X POST "https://icp.net/custom-domains/v1/yourdomain.com" | jq
# 4. Poll until registered
curl -sL -X GET "https://icp.net/custom-domains/v1/yourdomain.com" | jq
验证是否正常工作
# 1. Verify DNS records
dig CNAME yourdomain.com
# Expected: yourdomain.com. CNAME yourdomain.com.icp1.io.
dig TXT _canister-id.yourdomain.com
# Expected: "<your-canister-id>"
dig CNAME _acme-challenge.yourdomain.com
# Expected: _acme-challenge.yourdomain.com. CNAME _acme-challenge.yourdomain.com.icp2.io.
# 2. Verify ic-domains file is served by the canister
curl -sL "https://<canister-id>.icp.net/.well-known/ic-domains"
# Expected: your domain listed
# 3. Verify registration status is "registered"
curl -sL -X GET "https://icp.net/custom-domains/v1/yourdomain.com" | jq '.data.registration_status'
# Expected: "registered"
# 4. Verify the custom domain serves your canister
curl -sI "https://yourdomain.com"
# Expected: HTTP/2 200
# 5. Verify no stale ACME TXT records
dig TXT _acme-challenge.yourdomain.com
# Expected: no TXT records (only the CNAME)