Using custom domains
By default, all canisters on the Internet Computer are accessible via the icp0.io domain using their canister ID. In addition to this default setup, you can also make a canister available under a custom domain by registering it through the custom domains service of the HTTP gateways.
To do this, simply acquire a domain from any registrar (e.g., Namecheap or GoDaddy) and configure its DNS records as instructed. After that, the custom domains service handles the rest: it automatically obtains and renews SSL certificates, manages their expiration, and ensures proper SEO support.
These docs describe the API of the new, redesigned custom domain service. You can find the old docs here.
Register your domain as a custom domain
By following the steps below, you can host your canister under your own domain using the custom domain service provided by the HTTP gateways. First, the configuration steps are described, then an example is used to illustrate these steps, followed by some instructions on troubleshooting.
Step 1: Configure the DNS record of your domain, which is denoted with
CUSTOM_DOMAIN.Add a
CNAMEentry for your domain pointing toCUSTOM_DOMAIN.icp1.iosuch that all the traffic destined to your domain is redirected to the HTTP gateways.Add a
TXTentry containing the canister ID to the_canister-id-subdomain of your domain (e.g.,_canister-id.CUSTOM_DOMAIN);Add a
CNAMEentry for the_acme-challengesubdomain (e.g.,_acme-challenge.CUSTOM_DOMAIN) pointing to_acme-challenge.CUSTOM_DOMAIN.icp2.ioin order for the HTTP gateways to acquire the certificate.
Make sure to disable any certificate/SSL/TLS offering of your DNS provider (e.g., Universal SSL by Cloudflare) as they interfere with the custom domain registration and can even prevent certificate renewal.
In many cases, it is not possible to set a
CNAMErecord for the top of a domain, the Apex record. In this case, DNS providers support so-calledCNAMEflattening. To this end, these DNS providers offer flattened record types, such asANAMEorALIASrecords, which can be used instead of theCNAMEtoCUSTOM_DOMAIN.icp1.io. The custom DNS configuration guide provides detailed instructions for three popular registrars.Step 2: Create a file named
ic-domainsin your canister under the.well-knowndirectory containing the custom domain.Create a file named
ic-domainswithin your canister’s frontend source. The structure might look like this:├── dfx.json
├── package.json
├── src
│ ├── project_frontend
│ │ ├── src
│ │ │ ├── .well-known
│ │ │ │ └── ic-domainsMake sure the .well-known directory is part of your build output so it’s included when the canister is deployed.
Open the ic-domains file and list your custom domains—one per line. You can include multiple domains and subdomains:
custom-domain1.com
subdomain1.custom-domain1.com
subdomain2.custom-domain1.com
custom-domain2.com
subdomain1.custom-domain3.com
custom-domain4.comFor example,
subdomain1could stand forwww.By default,
dfxignores hidden files and directories (those starting with a dot). To ensure the.well-knowndirectory andic-domains file are deployed, create a file named.ic-assets.json5` in the same directory:├── dfx.json
├── package.json
├── src
│ ├── project_frontend
│ │ ├── src
│ │ │ ├── .ic-assets.json5
│ │ │ ├── .well-known
│ │ │ │ └──ic-domainsAdd the following configuration to
.ic-assets.json5:[
{
"match": ".well-known",
"ignore": false
}
]Step 3: Deploy the updated canister.
Step 4: (Optional) Validate your domain configuration before registering.
You can validate that your DNS records and canister configuration are correct by issuing the following command and replacing
CUSTOM_DOMAINwith your custom domain:curl -sL -X GET https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN/validate | jqAll commands also work without
jq.jqis simply a suggestion for convenience as it formats the JSON output nicely.If the validation is successful, you will get a JSON response like:
{
"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"
}
}If validation fails, you will get an error message indicating what needs to be fixed. Common validation errors include:
- Missing DNS CNAME record: The
CNAMEentry for the_acme-challengesubdomain is missing. - Missing DNS TXT record: The
TXTentry for the_canister-idsubdomain is missing. - Invalid DNS TXT record: The content of the
TXTentry is not a valid canister ID. - More than one DNS TXT record: There are multiple
TXTentries for the_canister-id-subdomain. Remove them and keep only one. - Failed to retrieve known domains: The
ic-domainsfile is not accessible under.well-known/ic-domains. - Domain is missing from list of known domains: The custom domain is missing from the
ic-domainsfile.
- Missing DNS CNAME record: The
Step 5: Register the domain with the HTTP gateways by issuing the following command and replacing
CUSTOM_DOMAINwith your custom domain.curl -sL -X POST https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN | jqIf the call was successful, you will get a JSON response like:
{
"status": "success",
"message": "Domain registration request accepted and may take a few minutes to process",
"data": {
"domain": "CUSTOM_DOMAIN",
"canister_id": "CANISTER_ID"
}
}In case the call failed, you will get an error response indicating the reason for the failure like:
{
"status": "error",
"message": "Domain registration request failed",
"data": {
"domain": "CUSTOM_DOMAIN"
},
"errors": "conflict: Another task for CUSTOM_DOMAIN is already in progress. Please retry after it completes."}Common errors include:
- bad_request: Invalid domain format, missing DNS records, or validation errors.
- conflict: Certificate already exists for this domain, or another task is already in progress.
- internal_server_error: An unexpected error occurred. Please try again later or contact support.
Step 6: Processing the registration can take several minutes.
Track the progress of your registration request by issuing the following command and replacing
CUSTOM_DOMAINwith your custom domain:curl -sL -X GET https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN | jqThe response will include a
registration_statusfield with one of the following values:registering: The registration request has been submitted and is being processed.registered: The domain has been successfully registered and has a valid certificate.expired: The domain registration has expired.failed: The registration request failed. The error message will indicate what went wrong.
Example response:
{
"status": "success",
"message": "Registration status of the domain",
"data": {
"domain": "CUSTOM_DOMAIN",
"canister_id": "CANISTER_ID",
"registration_status": "registering"
}
}Step 7: Once your registration status becomes
registered, wait a few minutes for the certificate to become available on all HTTP gateways.After that, you should be able to access your canister using the custom domain.
Example
Imagine you wanted to register your domain foo.bar.com for your canister with the canister ID hwvjt-wqaaa-aaaam-qadra-cai.
DNS configuration
| Record Type | Host | Value |
|---|---|---|
CNAME | foo.bar.com | foo.bar.com.icp1.io |
TXT | _canister-id.foo.bar.com | hwvjt-wqaaa-aaaam-qadra-cai |
CNAME | _acme-challenge.foo.bar.com | _acme-challenge.foo.bar.com.icp2.io |
Some DNS providers do not require you to specify the main domain (bar.com). For example:
fooinstead offoo.bar.com._canister-id.fooinstead of_canister-id.foo.bar.com._acme-challenge.fooinstead of_acme-challenge.foo.bar.com.
Step 1: Create the
ic-domainsfile with the following content in the.well-knowndirectory:foo.bar.comStep 2: Create the
.ic-assets.jsonfile at the root of the canister source:[
{
"match": ".well-known",
"ignore": false
}
]Step 3: Deploy the updated canister.
Step 4: (Optional) Validate your domain configuration.
curl -sL -X GET https://icp0.io/custom-domains/v1/foo.bar.com/validate | jqStep 5: Start the registration process.
curl -sL -X POST https://icp0.io/custom-domains/v1/foo.bar.com | jqStep 6: Check the registration status.
curl -sL -X GET https://icp0.io/custom-domains/v1/foo.bar.com | jq
Troubleshooting
When you are running into issues trying to register your custom domain, try the following troubleshooting steps:
Use the validate endpoint to check your DNS configuration and canister setup before attempting registration:
curl -sL -X GET https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN/validate | jqThis will provide detailed error messages about what needs to be fixed.
Check your DNS configuration using a tool like
digornslookup. For example, to check theTXTrecord with the canister ID, you can rundig TXT _canister-id.CUSTOM_DOMAIN. In particular, make sure that there are no extra entries (e.g., multipleTXTrecords for the_canister-id-subdomain).Check that there are no
TXTrecords for the_acme-challenge-subdomain(e.g., by usingdig TXT _acme-challenge.CUSTOM_DOMAIN). If there areTXTrecords, then they are most likely left over from previous ACME challenges by your domain provider. Note that these records often do not show up in your domain management dashboard. Try disabling all TLS/SSL-certificate offerings from your domain provider to remove these records.Check the
ic-domainsfile by downloading it directly from your canister (e.g., by openingCANISTER_ID.icp0.io/.well-known/ic-domainsin your browser or usingcurl CANISTER_ID.icp0.io/.well-known/ic-domainsin your terminal).Check the registration status to see the current state of your domain:
curl -sL -X GET https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN | jqThis will show you the current
registration_statusand any associated error messages.
You may need to specify a host in your frontend code when you are using a custom domain, as the HttpAgent may not be able to automatically infer the host like it can on icp0.io and icp0.io. To configure your agent, it will look something like this:
// Point to icp-api for the mainnet. Leaving host undefined will work for localhost
const host = isProduction ? "https://icp-api.io" : undefined;
const agent = await HttpAgent.create({ host });
Updating a custom domain
In case you want to update the domain to point to a different canister, you first need to update the DNS record of your domain, then notify the custom domains service:
Step 1: Update the
TXTentry to contain the new canister ID for the_canister-idsubdomain of your domain (e.g.,_canister-id.CUSTOM_DOMAIN).Step 2: Update the domain registration using a
PATCHrequest with the domain name in the path.curl -sL -X PATCH https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN | jqIf the call was successful, you will get a JSON response like:
{
"status": "success",
"message": "Update domain registration request accepted and may take a few minutes to process",
"data": {
"domain": "CUSTOM_DOMAIN",
"canister_id": "CANISTER_ID"
}
}Step 3: Check the registration status to track the update progress.
curl -sL -X GET https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN
Removing a custom domain
In case you want to remove your domain, you just need to remove the DNS records and notify the custom domains service:
Step 1: Remove the
TXTentry containing the canister ID for the_canister-idsubdomain (e.g.,_canister-id.CUSTOM_DOMAIN) and theCNAMEentry for the_acme-challengesubdomain (e.g.,_acme-challenge.CUSTOM_DOMAIN).Step 2: Notify the custom domains service of the removal using a
DELETErequest with the domain name in the path.curl -sL -X DELETE https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN | jqIf the call was successful, you will get a JSON response like:
{
"status": "success",
"message": "Delete domain registration request accepted and may take a few minutes to process",
"data": {
"domain": "CUSTOM_DOMAIN"
}
}Step 3: Check the registration status to confirm the deletion.
curl -sL -X GET https://icp0.io/custom-domains/v1/CUSTOM_DOMAINIf the domain has been successfully deleted, you will receive a
404 Not Foundresponse and a JSON body like:{
"status": "error",
"message": "Registration status request failed",
"data": {
"domain": "CUSTOM_DOMAIN"
},
"errors": "not_found: Domain CUSTOM_DOMAIN not found"
}
For frontends that use Internet Identity (II) to authenticate users, the principals provided by II depend on the domain from which the login request was started. If you authenticate your users through the canister URL and want to switch over to a custom domain, users will not have the same principals anymore. You can prevent this by setting up alternative origins.