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.
What you’ll have when you’re done
Section titled “What you’ll have when you’re done”- 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-revalidatewired 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.
Prerequisites
Section titled “Prerequisites”- A frontend deployed to a Front End Hosting – Advanced environment (deploy quickstart)
- An API client for your production site (auth guide)
- Your subscription’s entitlement confirmed (Front End Hosting)
-
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 > Actionson the golden path, or Code StudioSettings > CI/CD > Variables(markedMasked) if you use it. Build-time values are baked in atnext buildand 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_: creatingACQUIA_SITE_URLfails 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 isDRUPAL_*. - Runtime (the running app):
-
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-usThe
secretand 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 callingrevalidatePath). Keep the secret in an environment variable namedREVALIDATION_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-revalidatepath 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:
- Add a second environment variable (
REVALIDATION_SECRET_NEXT) with the new value, and make the handler accept either secret. Deploy. - Update the registered webhook URL (and any other callers of the route) to the new secret; confirm a test publish revalidates.
- 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.
- Add a second environment variable (
-
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-clientThen load the script before your server starts. On a standalone build, put the flag in the
startcommand 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 namedNODE_OPTIONSwith the value-r ./acquia-autoscaling.json the environment instead. Do not write it as a prefix inside thestartscript (NODE_OPTIONS='...' next start): astartscript 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-clientmust appear underdependenciesinpackage.jsonand ship in the artifact’snode_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. -
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 thekube-probe/1.31+andcurluser agents must return200, never behind auth.
-
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_IDandDRUPAL_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: 0and theacquia-force-revalidateendpoint are Front End Hosting – Advanced specifics; on Vercel or Netlify, use that platform’s own ISR/revalidation behavior.
When something goes wrong
Section titled “When something goes wrong”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.
Next steps
Section titled “Next steps”- 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?
What went wrong?
Still stuck? Contact Acquia Support (opens in a new tab)