Skip to main content

Using custom domains

Intermediate
Tutorial

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 CNAME entry for your domain pointing to CUSTOM_DOMAIN.icp1.io such that all the traffic destined to your domain is redirected to the HTTP gateways.

    • Add a TXT entry containing the canister ID to the _canister-id-subdomain of your domain (e.g., _canister-id.CUSTOM_DOMAIN);

    • Add a CNAME entry for the _acme-challenge subdomain (e.g., _acme-challenge.CUSTOM_DOMAIN) pointing to _acme-challenge.CUSTOM_DOMAIN.icp2.io in 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 CNAME record for the top of a domain, the Apex record. In this case, DNS providers support so-called CNAME flattening. To this end, these DNS providers offer flattened record types, such as ANAME or ALIAS records, which can be used instead of the CNAME to CUSTOM_DOMAIN.icp1.io. The custom DNS configuration guide provides detailed instructions for three popular registrars.

  • Step 2: Create a file named ic-domains in your canister under the .well-known directory containing the custom domain.

    Create a file named ic-domains within your canister’s frontend source. The structure might look like this:

    ├── dfx.json
    ├── package.json
    ├── src
    │ ├── project_frontend
    │ │ ├── src
    │ │ │ ├── .well-known
    │ │ │ │ └── ic-domains

    Make 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.com

    For example, subdomain1 could stand for www.

    By default, dfx ignores hidden files and directories (those starting with a dot). To ensure the .well-known directory and ic-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-domains

    Add 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_DOMAIN with your custom domain:

    curl -sL -X GET https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN/validate | jq

    All commands also work without jq. jq is 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 CNAME entry for the _acme-challenge subdomain is missing.
    • Missing DNS TXT record: The TXT entry for the _canister-id subdomain is missing.
    • Invalid DNS TXT record: The content of the TXT entry is not a valid canister ID.
    • More than one DNS TXT record: There are multiple TXT entries for the _canister-id-subdomain. Remove them and keep only one.
    • Failed to retrieve known domains: The ic-domains file is not accessible under .well-known/ic-domains.
    • Domain is missing from list of known domains: The custom domain is missing from the ic-domains file.
  • Step 5: Register the domain with the HTTP gateways by issuing the following command and replacing CUSTOM_DOMAIN with your custom domain.

    curl -sL -X POST https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN | jq

    If 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_DOMAIN with your custom domain:

    curl -sL -X GET https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN | jq

    The response will include a registration_status field 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 TypeHostValue
CNAMEfoo.bar.comfoo.bar.com.icp1.io
TXT_canister-id.foo.bar.comhwvjt-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:

  • foo instead of foo.bar.com.

  • _canister-id.foo instead of _canister-id.foo.bar.com.

  • _acme-challenge.foo instead of _acme-challenge.foo.bar.com.

  • Step 1: Create the ic-domains file with the following content in the .well-known directory:

    foo.bar.com
  • Step 2: Create the .ic-assets.json file 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 | jq
  • Step 5: Start the registration process.

    curl -sL -X POST https://icp0.io/custom-domains/v1/foo.bar.com | jq
  • Step 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 | jq

    This will provide detailed error messages about what needs to be fixed.

  • Check your DNS configuration using a tool like dig or nslookup. For example, to check the TXT record with the canister ID, you can run dig TXT _canister-id.CUSTOM_DOMAIN. In particular, make sure that there are no extra entries (e.g., multiple TXT records for the _canister-id-subdomain).

  • Check that there are no TXT records for the _acme-challenge-subdomain (e.g., by using dig TXT _acme-challenge.CUSTOM_DOMAIN). If there are TXT records, 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-domains file by downloading it directly from your canister (e.g., by opening CANISTER_ID.icp0.io/.well-known/ic-domains in your browser or using curl CANISTER_ID.icp0.io/.well-known/ic-domains in 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 | jq

    This will show you the current registration_status and 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 TXT entry to contain the new canister ID for the _canister-id subdomain of your domain (e.g., _canister-id.CUSTOM_DOMAIN).

  • Step 2: Update the domain registration using a PATCH request with the domain name in the path.

    curl -sL -X PATCH https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN | jq

    If 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 TXT entry containing the canister ID for the _canister-id subdomain (e.g., _canister-id.CUSTOM_DOMAIN) and the CNAME entry for the _acme-challenge subdomain (e.g., _acme-challenge.CUSTOM_DOMAIN).

  • Step 2: Notify the custom domains service of the removal using a DELETE request with the domain name in the path.

    curl -sL -X DELETE https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN | jq

    If 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_DOMAIN

    If the domain has been successfully deleted, you will receive a 404 Not Found response 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.