Skip to content

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.

  • 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.
  • 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_* or CANVAS_* 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)
  • jq locally, for one environment-ID lookup in step 3
  1. The platform runs npm start on exactly what you push, with PORT=3000 injected; it does not build your app. Each tab shows the framework’s production shape: the config, the start script, 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/standalone directory: server.js, a traced node_modules, your public/ and static assets copied in. next start with a full committed node_modules also works; standalone is far smaller.

    Whatever the framework, two more rules from the platform: keep start a plain command (a VAR=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
  2. 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), and ENVIRONMENT_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 build can 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.

  3. 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-wait

    Set CANVAS_SITE_URL for a Canvas Headless app, or the DRUPAL_* trio for a JSON:API app. Names may use letters, numbers, and underscores, and must not start with a number, AH_, or ACQUIA_.

    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"
    }
  4. 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 artifact step is the framework-specific line from step 1:

    .github/workflows/deploy-acquia.yml
    name: Deploy to Acquia
    on:
    push:
    branches: [main]
    jobs:
    build-and-deploy:
    runs-on: ubuntu-latest # Linux amd64, as Acquia requires
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    with:
    node-version: 22
    - name: Build
    env:
    DRUPAL_SITE_URL: ${{ secrets.DRUPAL_SITE_URL }}
    DRUPAL_CLIENT_ID: ${{ secrets.DRUPAL_CLIENT_ID }}
    DRUPAL_CLIENT_SECRET: ${{ secrets.DRUPAL_CLIENT_SECRET }}
    run: |
    npm ci
    npm run build
    - name: Assemble artifact
    run: |
    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 Acquia
    env:
    DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
    ACQUIA_GIT_URL: ${{ secrets.ACQUIA_GIT_URL }}
    run: |
    mkdir -p ~/.ssh
    echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_rsa
    chmod 600 ~/.ssh/id_rsa
    ssh-keyscan -H "$(echo "$ACQUIA_GIT_URL" | sed 's/.*@\([^:]*\):.*/\1/')" >> ~/.ssh/known_hosts
    cd build-output
    git init
    git config user.name "ci-bot"
    git config user.email "[email protected]"
    git remote add target "$ACQUIA_GIT_URL"
    git checkout -b main-build
    git add -A -f
    git commit -m "Build output from CI ($GITHUB_SHA)"
    git push target main-build -f
    - name: Trigger deploy via Cloud Platform API
    env:
    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.yml
    git commit -m "Configure Front End Hosting deploy"
    git push origin main

    Watch the run under your repository’s Actions tab. The green workflow run is not the end: the switch call creates a Code switched task 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.

  5. 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.

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.

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.

Was this page helpful?