Skip to content

Deploying a headless frontend

Goal: take a deployed frontend to production-grade on Front End Hosting – Advanced with secrets, ISR, and autoscaling configured inside the platform’s real constraints.

  • Production credentials stored in platform secret stores under the same three names as local development.
  • ISR that revalidates correctly across processes: memory cache at 0, acquia-force-revalidate wired up.
  • The autoscaling exporter running, and a deployment that respects the platform’s fixed port, npm-only builds, and no-SSH/no-Redis/no-log-forwarding constraints.
  1. Put production credentials in the platform’s secret stores, using the same names as local.

    Never commit production credentials; set them where the platform injects them. The variable names are the canon established in the auth quickstart and detailed in the auth guide: DRUPAL_SITE_URL, DRUPAL_CLIENT_ID, DRUPAL_CLIENT_SECRET, verbatim, in every store:

    • Runtime (the running app): acli api:environments:variable-create myapp.prod --name='DRUPAL_SITE_URL' --value='https://your-site.example.com' --task-wait, one per name, per environment. Changing a variable restarts the environment. Cloud Platform documents that these variables are not encrypted, so create a dedicated production API client with the minimum scope and rotate it as the auth guide describes.
    • Build-time (prerendering in CI): in your CI system’s secret store: GitHub Actions Settings > Secrets and variables > Actions on the golden path, or Code Studio Settings > CI/CD > Variables (marked Masked) if you use it. Build-time values are baked in at next build and stay fixed until the next deploy; runtime values are read on every request.

    Use a different site URL and client per environment; dev builds must not read production content. Every credential this step stores appears in the operations guide’s rotation inventory; before launch, walk the go-live checklist.

    Front End Hosting rejects environment variable names starting with ACQUIA_: creating ACQUIA_SITE_URL fails with “The environment variable name is invalid. It must contain only letters, numbers, and underscores; it cannot start with a number, AH_, or ACQUIA_; and it must be under 256 characters long”. That is why the canonical trio is DRUPAL_*.

  2. Configure ISR: set the memory cache to 0 before anything else.

    ISR on Front End Hosting – Advanced works only if the ISR memory cache size is set to 0. This is not tuning advice: each server runs at least two Node processes, and those processes share no memory. With an in-memory ISR cache, a revalidation updates one process’s cache while the others keep serving the old page. That leaves silently inconsistent content, varying per request. Setting the cache to 0 forces Next.js to read ISR data from the shared filesystem, which the platform provides for exactly this purpose.

    next.config.js
    module.exports = {
    // Required on Front End Hosting – Advanced: ≥2 Node
    // processes, no shared memory.
    cacheMaxMemorySize: 0,
    };

    With that in place, revalidate pages on demand through your app’s own revalidation route on the environment’s domain:

    https://<your-environment-domain>/api/revalidate?secret=<your-secret>&url=/about-us

    The secret and the route belong to your app’s own revalidation handler; if your app doesn’t have one yet, the webhooks guide builds exactly this route (a secret-checked handler calling revalidatePath). Keep the secret in an environment variable named REVALIDATION_SECRET (the name the rotation runbook below and the operations inventory use), not in code. One call to the route regenerates the page, and every request from both replicas then serves the identical new generation from the shared filesystem.

    Acquia’s documentation also describes an acquia-force-revalidate path prefix in front of the same route; requests to that prefix reach the app verbatim rather than being intercepted by the platform, so rely on your app’s own route.

    To revalidate automatically when editors change content, point a webhook at your revalidation route. See the webhooks guide.

    The revalidation secret is a credential and rotates like one, with one wrinkle: the registered webhook URL and the environment variable must change together or deliveries get rejected in between. Rotate with an overlap instead:

    1. Add a second environment variable (REVALIDATION_SECRET_NEXT) with the new value, and make the handler accept either secret. Deploy.
    2. Update the registered webhook URL (and any other callers of the route) to the new secret; confirm a test publish revalidates.
    3. Move the new value into REVALIDATION_SECRET, drop the fallback from the handler, deploy again.

    No delivery is rejected at any point: the handler always accepts the secret the webhook is currently sending.

  3. Enable the autoscaling exporter.

    Production environments autoscale based on traffic, but only if your app exposes metrics. The platform’s exporter is a script, acquia-autoscaling.js, placed at your project root and loaded before your server starts. It starts a Prometheus metrics server on port 9100 once your app listens on port 3000 (verified: with the platform’s injected port, the exporter starts and serves Prometheus metrics). Copy the script from Acquia’s Front End Hosting – Advanced getting started page, then:

    Terminal window
    npm install prom-client

    Then load the script before your server starts. On a standalone build, put the flag in the start command itself (verified: the exporter starts and serves metrics):

    {
    "scripts": {
    "start": "node --require ./acquia-autoscaling.js server.js",
    "build": "next build"
    }
    }

    With plain next start, set an environment variable named NODE_OPTIONS with the value -r ./acquia-autoscaling.js on the environment instead. Do not write it as a prefix inside the start script (NODE_OPTIONS='...' next start): a start script that begins with a variable assignment never executes on this platform. Acquia’s documentation shows the prefix form; use one of the two forms above.

    Either way, prom-client must appear under dependencies in package.json and ship in the artifact’s node_modules, or autoscaling is inoperative.

    With no SSH access, the success signal is the exporter’s startup line in the environment’s live log viewer in the Cloud Platform user interface. The app’s stdout may not reach the downloadable logs or acli app:log:tail, so use the Cloud UI log viewer for it. If the startup line is absent, re-check the two conditions above. Scaling limits are 16 replicas in production environments and 2 in non-production.

  4. Build within the platform’s constraints.

    These are fixed properties of Front End Hosting – Advanced. Design around them rather than fighting them:

    • Port 3000, always. The execution port is preset and cannot be modified. Any other port means traffic never reaches your app.
    • npm only. Yarn and pnpm are not supported for builds or dependency installs.
    • No SSH access to Node.js environments: debug with the environment logs (downloadable and streamable from the Cloud UI), not a shell.
    • No Redis or other databases. Cache in the CDN and the ISR filesystem; state lives in your CMS.
    • No log forwarding, no IP allow/deny lists, no custom load balancer configuration.
    • The readiness probe hits /. Requests to / from the kube-probe/1.31+ and curl user agents must return 200, never behind auth.
  5. If you deploy to another host instead.

    The app itself needs no code changes; only the variable store moves. Set the same three variables, DRUPAL_SITE_URL, DRUPAL_CLIENT_ID and DRUPAL_CLIENT_SECRET, in that host’s own environment variable store, scoped per deploy context so previews don’t read production content, then verify the live URL renders content exactly as in the deploy quickstart. Hosts such as Vercel and Netlify document their own build settings, variable stores, and deploy flow; follow their documentation for the specifics.

    cacheMaxMemorySize: 0 and the acquia-force-revalidate endpoint are Front End Hosting – Advanced specifics; on Vercel or Netlify, use that platform’s own ISR/revalidation behavior.

The classic symptom is works locally, fails deployed. Two different causes look identical from a distance; distinguish them first:

Application error: a server-side exception has occurred (page 500s, error in the environment’s server logs)

is a server-side failure: the environment variables are not set in the deploy target, so the token request goes to a malformed URL (the logs show a request to https:///oauth/token, the same symptom as an unloaded .env locally). Check: the environment’s Variables list shows all three canonical names (and your CI’s secret store too, if prerendered pages are the ones failing). Server-side requests are not subject to CORS; if the failure is in the server logs, it is not CORS.

Access to fetch at 'https://your-site.example.com/api/node/article' from origin 'https://www.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

is a browser-side failure: the page renders but client-side fetches die in the console. Your CMS’s CORS configuration doesn’t allow the production domain. Check: open the browser console on the live site; if this error appears there while the server logs are clean, it’s CORS. Fix: in your site’s admin UI, API > CORS configuration, add the production domain to Allowed origins and save.

Error: The module '/mnt/www/html/.../node_modules/sharp/build/Release/sharp.node' was compiled against a different Node.js version using NODE_MODULE_VERSION 115. This version of Node.js requires NODE_MODULE_VERSION 127.

is a build/runtime version mismatch: your CI built with one Node.js major and the environment runs another. Native modules compiled at build time break at runtime. Align the CI’s Node version (the setup-node version in the golden-path workflow, or NODE_VERSION in Code Studio) with the runtime version, and set "engines": { "node": "22" } in package.json so npm refuses a mismatched install.

  • Webhooks guide: trigger ISR revalidation from content changes instead of by hand.
  • CI/CD guide: tag-based production deploys and pipeline customization.
  • Front End Hosting: what the platform runs, and the entitlement check.

Was this page helpful?