Deploy a Canvas Headless app
Goal: take a Canvas Headless app, in any of its four frameworks, live on Front End Hosting – Advanced.
What you’ll have when you’re done
Section titled “What you’ll have when you’re done”- Your app live on a Front End Hosting – Advanced environment, serving on the platform’s port.
- A prebuilt artifact your CI can reproduce: built output,
node_modules, and one config file. - The one purge command that makes your first deploy actually visible.
Prerequisites
Section titled “Prerequisites”- A Cloud Platform subscription with a Front End Hosting – Advanced entitlement, and an application with environments (check what you have)
- An account whose team role includes repository access, with an RSA SSH key on your profile; the platform rejects ed25519 keys (never set one up? the BYO CI guide’s prerequisites walk through it)
- A Canvas Headless app from the Canvas Headless quickstart, with its site reachable
- Node.js 22 or 24 locally (the versions the platform runs)
-
Know what the platform runs
Section titled “Know what the platform runs”Front End Hosting – Advanced does not build your app. It checks out the git ref you deploy, and runs
npm startin it. Everything else follows from that:-
The artifact you push is the runnable app: built output,
package.jsonwith astartscript, and a committednode_modulesdirectory. An artifact withoutnode_modulesfails to deploy, even when your framework’s build output bundles its dependencies. -
The platform injects
PORT=3000and routes traffic to it. All four frameworks read it natively. -
The readiness probe connects over IPv6, so the server must listen on all interfaces including IPv6 (the
::bind). Next.js, Nuxt, and TanStack Start do this by default; Astro needs one config line (step 2). A server bound tolocalhostor an IPv4-only address runs fine while every deploy fails. -
Keep the
startscript a plain command (node server.js,next start). Deploys where the script carried an environment-variable prefix (HOST=:: node ...) failed with the app producing no output; put configuration in the build or in environment variables instead. -
One file at the artifact root tells the platform to skip its own build step:
acquia_config.yaml enable-prebuilt-artifact: true
-
-
Build the artifact from your scaffolded app
Section titled “Build the artifact from your scaffolded app”The Canvas Headless templates build without a reachable Canvas site (nothing is fetched or baked in at build time), so CI can build the artifact anywhere. Run the build on Linux amd64 so native packages in
node_modulesmatch the platform. Each tab lists what the scaffolded template already has, and the pieces to add.The template’s
next startalready runs the built server and reads the platform’s port. To make the checkout deployable:- Remove
/.next/and/node_modules/from.gitignore, build, and commit both (.next/cache/can stay ignored). - Add
acquia_config.yamlfrom step 1.
{"scripts": {"build": "next build","start": "next start"}}The committed
node_modulesis whatnext startruns from; sharp is its only platform-specific package, matched automatically when the build runs on Linux amd64. To ship a far smaller artifact, Next’s standalone mode also works on this platform (output: 'standalone'through the template’swithCanvas(),start: node server.js, copypublic/and.next/static/into the standalone directory after building).The template already uses the Node adapter in standalone mode. Two additions make it deployable, and the first one is not optional:
astro.config.mjs import { defineConfig } from 'astro/config';import node from '@astrojs/node';export default defineConfig({output: 'server',adapter: node({ mode: 'standalone' }),server: { host: '::' },});The adapter binds
localhostunless told otherwise, and its IPv4 forms (host: true,HOST=0.0.0.0) stay invisible to the platform’s IPv6 readiness probe: the server boots and every deploy fails.host: '::'listens on all interfaces.{"scripts": {"build": "astro build","start": "node ./dist/server/entry.mjs"}}Add the
startscript (the template has none), remove/dist/and/node_modules/from.gitignore, build, commit both (production dependencies only:npm install --omit=dev), and addacquia_config.yaml. Astro’s build output does not bundle dependencies, sonode_modulesships in full.The template’s
nuxt buildalready emits a self-contained server in.output/. To make the checkout deployable:- Add the
startscript below (the template has none). - Remove
/.output/and/node_modules/from.gitignore, build, and commit both (the platform requires anode_modulesdirectory even though.outputbundles everything the server imports). - Add
acquia_config.yaml.
{"scripts": {"build": "nuxt build","start": "node .output/server/index.mjs"}}The template builds only a request handler; no production server exists until Nitro is added. Three changes, verified end to end:
Terminal window npm install -D nitro// vite.config.ts: add the import, and the plugin after tanstackStart()import { nitro } from 'nitro/vite';// … the template's existing imports stayexport default defineConfig({plugins: [canvas(), tailwindcss(), tanstackStart(), nitro({ config: { preset: 'node-server' } }), viteReact()],});{"scripts": {"build": "vite build","start": "node .output/server/index.mjs"}}With the plugin,
vite buildemits.output/with a self-contained server that reads the platform’s port. Remove/.output/and/node_modules/from.gitignore, build, commit both, and addacquia_config.yaml.Read https://dev.acquia.com/source-cms/deploy/canvas-headless-hosting.md for the full guide, then make my Canvas Headless Next.js app deployable as a prebuilt artifact: un-ignore and commit .next and node_modules, keep the plain next start script, and add acquia_config.yaml with enable-prebuilt-artifact: true at the project root.Read https://dev.acquia.com/source-cms/deploy/canvas-headless-hosting.md for the full guide, then make my Canvas Headless Astro app deployable as a prebuilt artifact: add server: { host: '::' } to astro.config.mjs, add a plain node ./dist/server/entry.mjs start script, un-ignore and commit dist and a production node_modules, and add acquia_config.yaml with enable-prebuilt-artifact: true at the project root.Read https://dev.acquia.com/source-cms/deploy/canvas-headless-hosting.md for the full guide, then make my Canvas Headless Nuxt app deployable as a prebuilt artifact: add a plain node .output/server/index.mjs start script, un-ignore and commit .output and node_modules, and add acquia_config.yaml with enable-prebuilt-artifact: true at the project root.Read https://dev.acquia.com/source-cms/deploy/canvas-headless-hosting.md for the full guide, then make my Canvas Headless TanStack Start app deployable as a prebuilt artifact: add the nitro Vite plugin (node-server preset) after tanstackStart(), add a plain node .output/server/index.mjs start script, un-ignore and commit .output and node_modules, and add acquia_config.yaml with enable-prebuilt-artifact: true at the project root.The agent makes the same edits shown on the
Yourselftab for your framework; review the diff before you commit. - Remove
-
Set the runtime variables before the first deploy
Section titled “Set the runtime variables before the first deploy”On the environment page, add
CANVAS_SITE_URLpointing at your live Canvas site. Variable names on Front End Hosting may use letters, numbers, and underscores, and must not start with a number,AH_, orACQUIA_; theCANVAS_*names pass. The plain-Node servers do not read.envfiles, so the environment variable is the only control point in production.Order matters: the app fetches content on every request, so with no reachable Canvas site the homepage renders an error, the readiness probe fails, and the deploy itself fails. Set the variable first, then deploy. Make sure the site’s front page resolves to a published Canvas page, so
/renders 200 for the probe. -
Push the artifact to your application’s repository
Section titled “Push the artifact to your application’s repository”Your application’s git URL is on its overview page in the Cloud Platform user interface. Push the artifact as its own branch:
Terminal window git checkout -b deploy-artifactgit add -A -f && git commit -m "Build artifact"git push origin deploy-artifactIf the push is refused, the two causes worth checking first: your key is not RSA, or your team role lacks repository access. Both produce the same
Permission denied (publickey). -
Deploy the branch
Section titled “Deploy the branch”Terminal window acli api:environments:code-switch <environment-id> "deploy-artifact" --task-waitThe same action is
Switch codeon the environment page in the Cloud Platform user interface. The task takes 5 to 15 minutes. If it fails, the previous release keeps serving; nothing goes dark. -
Purge the cache, or keep seeing the old site
Section titled “Purge the cache, or keep seeing the old site”The platform fronts your environment with a cache, and a deploy does not purge it. On a new environment, the platform’s placeholder page carries a one-year cache lifetime, so your homepage keeps showing it even after a green deploy:
Terminal window acli api:environments:domain-clear-caches <environment-id> <your-environment-domain>Purge after every deploy. Cached responses from the previous release persist the same way, error pages included.
-
Verify
Section titled “Verify”Terminal window curl -s https://<your-environment-domain>/ | grep -o "<h1[^>]*>[^<]*</h1>"Your homepage’s heading in that output means the app is live and serving from the platform’s port.
What just happened
Section titled “What just happened”The platform checked out your branch, found enable-prebuilt-artifact: true, skipped its build system, and ran npm start with PORT=3000 injected. Your framework’s production server bound all interfaces, read the port from the environment, and answered the platform’s readiness probe on /. The purge step made the new release visible through the cache. Every later deploy is the same loop: build in CI, push the branch, switch, purge.
When something goes wrong
Section titled “When something goes wrong”The homepage still shows “Welcome to Acquia Cloud”
that is the platform’s placeholder, cached with a one-year lifetime, not a failed deploy. It appears on every new environment (its copy talks about Drupal; on this product, ignore it). Run the purge from step 6.
Code switched task fails while the site keeps working
the previous release keeps serving through a failed deploy, and the environment’s deployed ref still moves to the new branch. In order of likelihood, verified causes: the server binds localhost or IPv4 only (Astro without server: { host: '::' }); the artifact has no committed node_modules; the start script carries an environment-variable prefix or does not run the built server; acquia_config.yaml is missing from the artifact root; the app’s content backend is unreachable so / errors and the probe fails.
Changed a variable and the app has not picked it up
variable changes apply through a platform task, and the app restarts with the new values about a minute after the task completes. If it never appears, check the name against the naming rule in step 3.
Permission denied (publickey) on push
the platform accepts RSA keys only, and a key on your profile is not enough; your team role must include repository access.
Provisioning is automatic: signing the contract creates the Front End Hosting application with its environments (a verified new application came with Prod and Staging, Node.js 22 preselected). If your application is missing, file a support ticket.
Next steps
Section titled “Next steps”- Front End Hosting: what the platform runs, and the entitlement check.
- Deploy from your own CI/CD: the same artifact loop run by CI on every push, with multiple environments and a gated production deploy.
- Canvas Headless quickstart: scaffold the app this page deploys.
Was this page helpful?
What went wrong?
Still stuck? Contact Acquia Support (opens in a new tab)