For AI agents: Documentation index at /llms.txt

Skip to content

Deploy a static site

Deploy a static site (a built frontend, docs, or any folder of files) to a canister on the Internet Computer that serves it over HTTP with response certification. You point icp-cli at your build directory and run icp deploy: it uploads the files, and the canister certifies and serves them.

Certification is what makes this different from a plain web host: every response the canister returns carries a cryptographic proof, and a verifying HTTP gateway checks that proof before handing the response to the browser. Visitors get content the canister has provably committed to, with nothing in between able to tamper with it in transit.

The canister certifies every response, always. Whether that proof gets checked depends on the gateway a visitor goes through, so link to one that verifies and treat raw URLs as a debugging tool.

You configure it through icp-cli using a recipe, @dfinity/static-site, which pins a matched pair of the canister and the sync plugin that uploads to it. Both are released from dfinity/certified-assets, which is also where the Candid interface and the build artifacts live. Most sites need nothing more than this page.

You will need icp-cli, and somewhere to deploy: a local network for a first look (icp network start -d), or a configured environment for a live one. Both are covered in the icp-cli docs.

canisters:
- name: frontend
recipe:
type: "@dfinity/static-site@<version>"
configuration:
dir: dist # the directory of files to serve

Replace <version> with a released version (e.g. v0.3.3); see the available versions. Pick the version here: because the recipe pins a matched canister + plugin pair, there is no separate canister version to choose.

Terminal window
icp deploy

This installs the canister, then runs the sync plugin to upload and certify every file in dir. Re-running icp deploy syncs again: the plugin diffs your directory against the canister and uploads only what changed.

Open the URL icp deploy prints and your site is there, certified.

The recipe takes four configuration fields:

FieldTypeRequiredDescription
dirstringYesThe single directory of built files to serve. The canister owns its whole URL space, so this is one directory, not a list.
buildarrayNoShell commands run before the canister exists to produce dir (e.g. npm run build). No canister IDs are available yet.
presyncarrayNoShell commands run at sync time, after the canister exists, with the deployed canister IDs exported as env vars (ICP_CLI_CID, ICP_CLI_CID_<NAME>, ICP_CLI_NETWORK, ICP_CLI_ENVIRONMENT). Use it to build a frontend that must bake in a canister ID.
metadataarrayNoname/value pairs baked into the canister wasm via ic-wasm.

A fuller example with a build step and metadata:

canisters:
- name: frontend
recipe:
type: "@dfinity/static-site@<version>"
configuration:
build:
- npm ci
- npm run build
dir: dist
metadata:
- name: "frontend:framework"
value: "react"

metadata is the only field that needs ic-wasm. It ships with icp-cli, so if you installed the CLI you already have it.

Building against canister IDs (presync vs build)

Section titled “Building against canister IDs (presync vs build)”

build runs before the canister is created, so it can’t know any canister IDs. When a client-side app needs to embed the ID of a canister it will call, build it in presync instead. That runs at sync time, once the IDs exist, and exports them to your commands:

canisters:
- name: frontend
recipe:
type: "@dfinity/static-site@<version>"
configuration:
dir: dist
presync:
- npm ci
# $ICP_CLI_CID_BACKEND is the `backend` canister's principal.
- VITE_CANISTER_ID_BACKEND=$ICP_CLI_CID_BACKEND npm run build

The variables available to presync: ICP_CLI_CID (this canister’s principal), ICP_CLI_CID_<NAME> (each project canister’s principal, with the name upper-cased and non-alphanumerics replaced by _, e.g. backendICP_CLI_CID_BACKEND), ICP_CLI_NETWORK, and ICP_CLI_ENVIRONMENT.

You don’t have to configure any of these; they’re on by default:

  • Response certification. Every response carries a proof, checked by a verifying gateway.
  • Clean URLs. /about serves /about.html, /blog/ serves /blog/index.html, with redirects that keep one canonical URL per page.
  • Compression. Text, JS, JSON, SVG, and wasm are stored gzip- and Brotli-compressed and negotiated per request via Accept-Encoding.
  • ETag / 304 Not Modified. Each asset gets a content-hash ETag, so unchanged files aren’t re-downloaded.
  • A default 404 page. A built-in, certified fallback you can replace by adding your own /404.html.

When you need finer control, each topic has its own page:

  • Routing & clean URLs. How request paths map to files, trailing slashes, and 404 handling.
  • Single-page apps. The one _redirects rule a client-side-routed app needs, so every URL loads (and reloads) your shell.
  • Redirects & rewrites. The _redirects file: send /old to /new, serve one file at another path, set custom error pages.
  • Custom headers. The _headers file: cache-control, a Content Security Policy, and other security headers.
  • Site files & conventions. What gets uploaded, the special _redirects/_headers files, excluded files, and custom domains.
  • Access protection. Put a login screen in front of a private/preview app with revocable, expiring access tokens.
  • Verifying contents. The canister’s state hash: prove to a third party that it serves exactly a known build, from source.

Curious how it works underneath? See Under the hood.