Quickstart
Goal: take your app, in any of the four supported frameworks, from a GitHub repository to a live URL on Front End Hosting – Advanced, redeploying on every push.
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.
- A GitHub Actions workflow that rebuilds the artifact and redeploys it on every push.
- The purge command that makes every deploy visible through the platform’s cache.
Prerequisites
Section titled “Prerequisites”- Your app in a GitHub repository: a Canvas Headless app from the Canvas Headless quickstart, the First fetch app, or any app in one of the four frameworks below
- A Cloud Platform application with a Front End Hosting – Advanced entitlement (check yours)
- Cloud Platform API credentials (client ID + secret) from the Cloud Platform API page; these are account-level credentials, distinct from your site’s
DRUPAL_*orCANVAS_*pair - An RSA SSH key pair whose public key is on your Acquia profile, on an account whose team role includes repository access (never set one up? the BYO CI guide’s prerequisites walk through it), plus your application’s Acquia git URL (on the application’s overview page in the Cloud Platform user interface)
jqlocally, for one environment-ID lookup in step 3
-
Shape the app into a deployable artifact
Section titled “Shape the app into a deployable artifact”The platform runs
npm starton exactly what you push, withPORT=3000injected; it does not build your app. Each tab shows the framework’s production shape: the config, thestartscript, and the artifact directory your CI will assemble in step 4.next.config.js module.exports = { output: 'standalone' };{"scripts": {"build": "next build && cp -r public .next/standalone/public && cp -r .next/static .next/standalone/.next/static","start": "node server.js"}}The artifact is the
.next/standalonedirectory:server.js, a tracednode_modules, yourpublic/and static assets copied in.next startwith a full committednode_modulesalso works; standalone is far smaller.astro.config.mjs import { defineConfig } from 'astro/config';import node from '@astrojs/node';export default defineConfig({output: 'server',adapter: node({ mode: 'standalone' }),server: { host: '::' },});{"scripts": {"build": "astro build","start": "node ./dist/server/entry.mjs"}}server: { host: '::' }is required: the adapter’s default bindslocalhost, which the platform’s readiness probe (IPv6) cannot reach, and every deploy fails while the server runs. The artifact isdist/plus a productionnode_modules(npm install --omit=dev); Astro’s output does not bundle dependencies.nuxt.config.ts export default defineNuxtConfig({nitro: { preset: 'node-server' },});{"scripts": {"build": "nuxt build","start": "node .output/server/index.mjs"}}The artifact is
.output/pluspackage.json, plus a committednode_modules: the platform requires the directory even though.outputbundles everything the server imports.// vite.config.ts: add the import, and the plugin after tanstackStart()import { nitro } from 'nitro/vite';// … the existing imports stayexport default defineConfig({plugins: [tanstackStart(), nitro({ config: { preset: 'node-server' } }), viteReact()],});{"scripts": {"build": "vite build","start": "node .output/server/index.mjs"}}Without the Nitro plugin,
vite buildemits only a request handler with no production server. With it, the artifact is.output/pluspackage.jsonplus a committednode_modules, as with Nuxt.Read https://dev.acquia.com/source-cms/deploy/quickstart.md for the full guide, then prepare my Next.js app for a prebuilt-artifact deploy: set output: 'standalone' in next.config.js, add a next build build script and a plain node server.js start script, and add acquia_config.yaml with enable-prebuilt-artifact: true at the project root.Read https://dev.acquia.com/source-cms/deploy/quickstart.md for the full guide, then prepare my Astro app for a prebuilt-artifact deploy: set output: 'server' with the @astrojs/node adapter in standalone mode and server: { host: '::' } in astro.config.mjs, add a plain node ./dist/server/entry.mjs start script, and add acquia_config.yaml with enable-prebuilt-artifact: true at the project root.Read https://dev.acquia.com/source-cms/deploy/quickstart.md for the full guide, then prepare my Nuxt app for a prebuilt-artifact deploy: pin the node-server Nitro preset in nuxt.config.ts, add a plain node .output/server/index.mjs start script, and add acquia_config.yaml with enable-prebuilt-artifact: true at the project root.Read https://dev.acquia.com/source-cms/deploy/quickstart.md for the full guide, then prepare my TanStack Start app for a prebuilt-artifact deploy: add the nitro Vite plugin (node-server preset) after tanstackStart() in vite.config.ts, add a plain node .output/server/index.mjs start script, and add acquia_config.yaml with enable-prebuilt-artifact: true at the project root.The agent makes the same config,
package.json, andacquia_config.yamledits shown on theYourselftab; review the diff before you commit.Whatever the framework, two more rules from the platform: keep
starta plain command (aVAR=x node ...prefix never executes), and add one file at the artifact root so the platform skips its own build step:acquia_config.yaml enable-prebuilt-artifact: true -
Set the secrets
Section titled “Set the secrets”In GitHub, under
Settings > Secrets and variables > Actions, add the deploy secrets:DEPLOY_SSH_KEY(the private SSH key),ACQUIA_GIT_URL,CLIENT_ID/CLIENT_SECRET(the Cloud Platform API pair), andENVIRONMENT_ID. The BYO CI guide’s secrets table defines each one.If your app prerenders content at build time, add its site credentials too, so
next buildcan fetch:DRUPAL_SITE_URL,DRUPAL_CLIENT_ID,DRUPAL_CLIENT_SECRET. The First fetch app does prerender; the Canvas Headless templates fetch at request time instead. -
Set the runtime variables
Section titled “Set the runtime variables”Add what your app reads at request time to the Cloud Platform environment, one command per variable:
Terminal window acli api:environments:variable-create myapp.prod \--name='CANVAS_SITE_URL' --value='https://your-site.example.com' --task-waitSet
CANVAS_SITE_URLfor a Canvas Headless app, or theDRUPAL_*trio for a JSON:API app. Names may use letters, numbers, and underscores, and must not start with a number,AH_, orACQUIA_.Do this before the first deploy: an app whose content backend is unreachable renders errors on
/, and the readiness probe then fails the deploy itself.Find the environment ID for the workflow’s secret:
Terminal window acli api:applications:environment-list myapp \| jq '.[] | {name, id}'{"name": "dev","id": "123456-a1b2c3d4-5678-90ab-cdef-1234567890ab"} -
Add the deploy workflow and push
Section titled “Add the deploy workflow and push”One workflow builds the app, assembles the artifact, pushes it to your application’s Acquia git remote, and triggers the deploy through the Cloud Platform API. The
Assemble artifactstep is the framework-specific line from step 1:.github/workflows/deploy-acquia.yml name: Deploy to Acquiaon:push:branches: [main]jobs:build-and-deploy:runs-on: ubuntu-latest # Linux amd64, as Acquia requiressteps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with:node-version: 22- name: Buildenv:DRUPAL_SITE_URL: ${{ secrets.DRUPAL_SITE_URL }}DRUPAL_CLIENT_ID: ${{ secrets.DRUPAL_CLIENT_ID }}DRUPAL_CLIENT_SECRET: ${{ secrets.DRUPAL_CLIENT_SECRET }}run: |npm cinpm run build- name: Assemble artifactrun: |rm -rf build-output && mkdir build-output# Next.js (standalone): cp -R .next/standalone/. build-output/# Astro: cp -R dist node_modules package.json build-output/# Nuxt: cp -R .output node_modules package.json build-output/# TanStack Start: cp -R .output node_modules package.json build-output/cp -R .next/standalone/. build-output/cp acquia_config.yaml build-output/- name: Push artifact to Acquiaenv:DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}ACQUIA_GIT_URL: ${{ secrets.ACQUIA_GIT_URL }}run: |mkdir -p ~/.sshecho "$DEPLOY_SSH_KEY" > ~/.ssh/id_rsachmod 600 ~/.ssh/id_rsassh-keyscan -H "$(echo "$ACQUIA_GIT_URL" | sed 's/.*@\([^:]*\):.*/\1/')" >> ~/.ssh/known_hostscd build-outputgit initgit config user.name "ci-bot"git config user.email "[email protected]"git remote add target "$ACQUIA_GIT_URL"git checkout -b main-buildgit add -A -fgit commit -m "Build output from CI ($GITHUB_SHA)"git push target main-build -f- name: Trigger deploy via Cloud Platform APIenv:CLIENT_ID: ${{ secrets.CLIENT_ID }}CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }}ENVIRONMENT_ID: ${{ secrets.ENVIRONMENT_ID }}run: |ACCESS_TOKEN=$(curl --silent -L --request POST \--url "https://accounts.acquia.com/api/auth/oauth/token" \--header "content-type: application/x-www-form-urlencoded" \--data grant_type=client_credentials \--data client_id="${CLIENT_ID}" \--data-urlencode client_secret="${CLIENT_SECRET}" | jq -r .access_token)curl --silent --fail -X POST \"https://cloud.acquia.com/api/environments/${ENVIRONMENT_ID}/code/actions/switch" \-H "Authorization: Bearer $ACCESS_TOKEN" \-H "Content-Type: application/json" \-H "Accept: application/hal+json" \-d '{"branch": "main-build"}'Commit and push:
Terminal window git add package.json acquia_config.yaml .github/workflows/deploy-acquia.ymlgit commit -m "Configure Front End Hosting deploy"git push origin mainWatch the run under your repository’s
Actionstab. The green workflow run is not the end: the switch call creates aCode switchedtask on the environment, and the code goes live when that task completes, 5 to 15 minutes later. Open the target environment in the Cloud UI and watch the task complete before verifying. If the task fails, the previous release keeps serving; nothing goes dark. -
Purge the cache and verify
Section titled “Purge the cache and verify”The platform’s cache survives deploys, and a new environment’s placeholder page is cached with a one-year lifetime, so purge after every deploy:
Terminal window acli api:environments:domain-clear-caches <environment-id> <your-environment-domain>Then request the environment’s default URL (on the environment page in the Cloud UI):
Terminal window curl -s https://myappdev.prod.acquia-sites.com/ \| grep -o "<h1[^>]*>[^<]*</h1>"Your homepage’s heading in that output means your app is live and serving.
What just happened
Section titled “What just happened”Your CI did the whole deploy in two moves the platform understands. The artifact push is plain git: because enable-prebuilt-artifact: true ships inside the artifact, the platform serves what you pushed instead of building it. The deploy trigger is one Cloud Platform API call (POST /api/environments/{environmentId}/code/actions/switch) authorized by a client_credentials token from accounts.acquia.com. At request time the platform runs npm start with PORT=3000 injected, which your framework’s server reads from the environment. Every push to main repeats the run; the purge makes each release visible.
When something goes wrong
Section titled “When something goes wrong”The homepage still shows “Welcome to Acquia Cloud”
the platform placeholder, cached for up to a year, not a failed deploy. It appears on every new environment (its copy talks about Drupal; ignore that). Purge per step 5.
Code switched task fails while the previous release keeps serving
the verified causes, in order of likelihood: 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.
Permission denied (publickey). in the Push artifact step
the key is not RSA (the platform rejects ed25519), the DEPLOY_SSH_KEY secret doesn’t match a key on your Acquia profile, or your team role lacks repository access.
error in the token response from accounts.acquia.com
the Cloud Platform client ID or secret is wrong, or the credentials were revoked. Regenerate them per the Cloud Platform API page; these are not your site’s content credentials.
A variable change hasn’t reached the app
variable updates apply through a platform task, and the app restarts with the new values about a minute after the task completes.
Next steps
Section titled “Next steps”- Deploy a Canvas Headless app: the same artifact shapes deployed by hand, with the template-specific details per framework.
- Deploying a headless frontend: production secrets, ISR mechanics, autoscaling, and the platform’s constraints.
- Deploy from your own CI/CD: this same flow with multiple environments and a gated production deploy.
Was this page helpful?
What went wrong?
Still stuck? Contact Acquia Support (opens in a new tab)