Docs menu

Deploying Next.js

Deploy a Next.js app on Primcloud: Procfile, environment variables, standalone output, and framework-specific gotchas.

On this page

Next.js runs on Primcloud with no framework-specific setup. The standard Node.js toolchain handles it. Build with next build in your build script, run next start from a Procfile, and let Next serve. This page covers the pieces that trip up most Next.js deploys on Primcloud specifically: Procfile shape, the env vars Next expects, when to use output: 'standalone', and the framework quirks that matter at runtime.

Starter template#

The Vercel create-next-app scaffold works with no modifications beyond adding a Procfile and pinning engines.node:

Terminal
npx create-next-app@latest my-app
cd my-app

Then add the two files below, commit, and import into Primcloud.

Procfile for Next.js on Primcloud#

Procfile
web: next start -p $PORT

Important

next start reads $PORT when passed via -p. The -p $PORT flag is required. Without it, Next binds to 3000 and Primcloud can't reach the pod.

With output: 'standalone' (see Framework gotchas below), the launch command changes:

Procfile
web: node .next/standalone/server.js

The standalone server.js reads $PORT on its own, no flag needed.

Background workers. If you run any background work (queued jobs) as separate processes, add a worker line:

Procfile
web: next start -p $PORT
worker: node dist/worker.js

The worker formation runs alongside web and is scaled independently. See Pod Formations.

Env vars this framework expects#

Set these in the Secrets panel on the dashboard.

Required-ish:

  • NODE_ENV=production: Next automatically sets this internally, but any code you write should also treat production as the default. Setting it explicitly makes intent clear.
  • Any NEXT_PUBLIC_* values your components read at build time. These are baked into the client bundle during next build. They must be set on the environment before the deploy that builds them, not after. Changing a NEXT_PUBLIC_* value requires a rebuild.

Common:

  • NEXTAUTH_URL / NEXTAUTH_SECRET: required if you're using NextAuth / Auth.js. NEXTAUTH_URL should match your production domain.
  • DATABASE_URL: Prisma, Drizzle, and anything else expects it under this name by convention.
  • NEXT_TELEMETRY_DISABLED=1: opts out of Next.js's build-time telemetry ping.

Runtime vs. build-time. Next.js exposes only NEXT_PUBLIC_* variables to client code, and it does so at build time. The values are inlined into the JS bundles when next build runs. Server-side code (route handlers, server components, getServerSideProps) reads any env var at runtime.

Framework gotchas on Primcloud#

Use output: 'standalone' for smaller images.

Next.js has an official standalone output mode that traces exactly which files your app needs at runtime and emits a minimal Node.js server:

next.config.js
module.exports = {
  output: 'standalone',
}

With this on, next build produces .next/standalone/, a self-contained runtime folder that includes server.js, your compiled code, and only the node_modules your app actually imports. The launch command becomes node .next/standalone/server.js.

With pnpm or Yarn, the pruning Primcloud does after your build removes dev dependencies from node_modules; with npm they stay installed. Standalone output goes further: it removes everything Next.js's tracer can prove is unused. On a large Next.js app, this is the difference between a 500 MB runtime image and a 50 MB one.

You'll also need to copy the public/ and .next/static/ directories into the standalone tree. Next.js doesn't do this automatically. Add a heroku-postbuild step:

package.json
{
  "scripts": {
    "build": "next build",
    "heroku-postbuild": "cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/"
  }
}

Static assets and the public/ directory.

Files in public/ are served by Next.js at the root path. On standalone builds, you must copy public/ into .next/standalone/ as above. Next.js's default next start handles this automatically, standalone does not.

For truly static assets that don't need to go through Next.js, consider serving them from a CDN in front of Primcloud rather than through the Node.js process.

Image optimization.

Next.js's <Image> component does on-demand optimization by default. On Primcloud this runs in your Node.js process, which uses CPU and memory. Two options:

  • Cache the optimizations. Next.js writes optimized images to .next/cache/images/. This directory persists across deploys but not across pod restarts, so you'll pay a per-request cost the first time each image is requested after a restart.
  • Use next.config.js images.unoptimized: true to disable optimization entirely and let the browser handle sizing.

Middleware and Edge runtime.

Next.js middleware.ts runs on Node.js on Primcloud (there is no Edge runtime here). Middleware code that depends on Edge-only APIs (e.g., Vercel's waitUntil) won't work. Stick to the Node.js runtime for middleware.

Build memory.

next build on a large app can peak at several GB of RAM (mostly TypeScript type-checking and bundling). If the build runs out of memory, set NODE_OPTIONS=--max-old-space-size=4096 in the Secrets panel to raise V8's heap limit.

Troubleshooting#

Build fails at Type error:: Next.js runs the TypeScript compiler as part of next build. Fix the error, or set typescript.ignoreBuildErrors: true in next.config.js (not recommended; hides real bugs).

Client requests hit a 500 with Application error: a server component or route handler threw. Check the runtime log for the stack trace, then reproduce locally with npm run build && npm start (not npm run dev, since dev mode surfaces different errors).

NEXT_PUBLIC_* env var changed but the client still sees the old value: these are baked in at build time. Trigger a rebuild by pushing a new commit, or use the manual redeploy option from the Deployments tab.

next start binds to port 3000 instead of $PORT: you forgot the -p $PORT flag in the Procfile, or you're on standalone output and using next start instead of node .next/standalone/server.js.

Standalone output can't find public/ or .next/static/: you didn't copy them into .next/standalone/ in heroku-postbuild. See the snippet in Framework gotchas.