Deploy Next.js on Zenifra
Next.js runs on Zenifra as a full Node.js server: server rendering, API routes, Server Actions, middleware, and image optimization work without adapters.
Prerequisites
- A GitHub repository with the Next.js project
package.jsonandpackage-lock.jsoncommitted at the repository root- GitHub account connected to Zenifra
- The project runs locally with
npm ci && npm run build && npm start
pnpm and Yarn
Install uses npm ci. If the project uses pnpm or Yarn, generate and commit a compatible package-lock.json, or deploy via OCI Image.
Console configuration
Create the project
In the console, click Create Project and choose GitHub Repository. Select the repository and branch.
Pick the runtime
Select Node.js and the same major version you use locally (20, 22, or 24). Check the engines field in package.json and the minimum version required by your Next.js release.
Fill in port and commands
| Field | Value |
|---|---|
| Port | 3000 |
pre-build | empty |
build | npm run build |
start | npx next start -H 0.0.0.0 -p 3000 |
Add variables
Add environment variables before creating the project, including NEXT_PUBLIC_*. See the next section.
Create and follow along
Click Create Project and watch the Build Logs tab. When the project is Running, open the *.clients.zenifra.com URL.
Environment variables in Next.js
Next.js treats two kinds of variables differently:
| Kind | Example | When it is read |
|---|---|---|
| Server | DATABASE_URL, AUTH_SECRET | At runtime |
| Public | NEXT_PUBLIC_API_URL | At build time, embedded in browser JavaScript |
On Zenifra, project variables are available during the build, so NEXT_PUBLIC_* works normally.
Changed a NEXT_PUBLIC_ value? Rebuild
Saving variables restarts the instances but does not recompile the code. NEXT_PUBLIC_* values already embedded for the browser only change after a new deployment: push a commit or trigger a manual deploy.
Never put secrets in NEXT_PUBLIC_* variables: every visitor can see them.
Health check
Create a lightweight route for the health check:
export const dynamic = 'force-dynamic';
export function GET() {
return Response.json({ status: 'ok' });
}Set the path to /api/health in the project. If middleware.ts enforces authentication, exclude this route in the matcher so the check does not get redirected.
Database with Prisma or Drizzle
- Create a managed database and copy its connection URL.
- Add
DATABASE_URLto the project variables. - Make sure the ORM client is generated during the build. With Prisma,
prisma generateis usually inpostinstall; if not, usebuild=npx prisma generate && npm run build. - Apply migrations at startup, when the app already has access to the project network:
start: npx prisma migrate deploy && npx next start -H 0.0.0.0 -p 3000Multiple instances
With more than one instance, all of them run start. Prisma serializes migrate deploy with a database lock, but long migrations delay startup. For large changes, apply the migration beforehand from a machine with database access.
User uploads
The instance filesystem is ephemeral. Uploads saved to public/ or to disk disappear on every deployment. Use persistent storage with a directory outside the code, such as /app/uploads, or an object storage service.
Next.js cache and multiple instances
The Next.js data cache and ISR live on each instance's disk by default. With more than one instance, each keeps its own cache, and revalidatePath only affects the instance that received the call. If that matters to you, use one instance or configure a shared cacheHandler backed by the managed cache.
standalone output (optional)
output: 'standalone' reduces the size of the running app. If you enable it, adjust the commands:
build: npm run build && cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/
start: node .next/standalone/server.jsAlso set the HOSTNAME=0.0.0.0 and PORT=3000 variables in the project, which server.js uses to listen.
Common problems
| Symptom | Likely cause | Fix |
|---|---|---|
Build fails with Cannot find module 'typescript' | Build dependencies removed | Make sure the build field is filled in and package-lock.json is in sync |
npm ci fails | Missing or out-of-sync lockfile | Run npm install, commit package-lock.json, and deploy again |
| Public URL does not respond | Different port or server on localhost | Use -H 0.0.0.0 -p 3000 and Port 3000 |
NEXT_PUBLIC_* has an old value | Value embedded in the previous build | Deploy again |
next/image returns errors | Remote domain not allowed | Add the domain to images.remotePatterns |
| Slow build or out of memory | Large project | Review the project plan in Limits |
Next steps
Last updated on
Framework guides
Pick your framework and see the runtime, port, build and start commands, and the recommended path to deploy on Zenifra via GitHub or an OCI image.
Node.js APIs with Express, Fastify, and NestJS
Configure Node.js APIs on Zenifra with Express, Fastify, or NestJS: port, commands, TypeScript, migrations, WebSockets, health check, and common errors.