Node.js apps
Deploy Next.js, Nuxt, NestJS, Express and other Node.js servers with the right Node version, PORT, persistent files, cluster mode, rollback and logs.
ZoPanel runs a Node.js server as a systemd service of the hosting account, behind nginx, which handles the domain, SSL and request limits. Use this page when your project has a package.json and runs a server process. For front-end builds that produce only static files (Vite, Create React App, Angular, Next.js static export), see Static sites. For the general deployment pipeline shared by all languages, see Git deploy.
Prerequisites
- At least one Node.js version installed on the server (the installer adds Node.js 22 by default).
- A website of type Git deploy, or any website with its Deploy tab (for example Node / Proxy).
- The account's package must include Git deploy & apps. Otherwise the panel answers "this feature is not included in your hosting package".
Install Node.js versions (administrator)
- Open Runtimes in the admin menu.
- Under Other runtimes, find the Node.js card.
- Click Node 24, Node 22, Node 20 or Node 18. Several versions can be installed side by side.
ZoPanel downloads the latest release of that major version from nodejs.org, verifies its SHA-256 checksum, installs it in /opt/zopanel/runtimes/node/<major> and adds pnpm and yarn. Installed versions can be removed from the same card.
Note: Node.js 18 and 20 have reached end of life in the official Node.js release schedule. Use 22 or 24 for new projects.
Which version an app uses
| Source (first found wins) | Example | Result |
|---|---|---|
.nvmrc |
22 or v22.11.0 |
major 22 |
.node-version |
24.1.0 |
major 24 |
engines.node in package.json |
">=20" |
major 20 (the first number) |
| none of the above | the newest installed version |
If the requested major version is not installed, the newest installed version is used. You can also pin a version yourself: turn Auto-detect off in the Build & run card and pick it under Version (Latest installed = the newest one).
Tip: engines.node: ">=18" selects Node.js 18 when it is installed. Write the major you actually want in .nvmrc.
Detection
On every deploy, unless Auto-detect is off, ZoPanel reads package.json and fills in the build settings. A project with composer.json is treated as PHP unless it depends on a Node server framework. Likewise, a Python web app (with manage.py, or django, flask, fastapi, uvicorn or gunicorn in its Python dependency files) is treated as Python when package.json has neither a start script nor a Node server framework; see Python apps.
Package manager: pnpm-lock.yaml (or "packageManager": "pnpm@…") → pnpm install --frozen-lockfile; yarn.lock (or yarn@…) → yarn install --frozen-lockfile; package-lock.json or npm-shrinkwrap.json → npm ci; otherwise npm install. A bun lockfile is installed with npm install. The build command is <pm> run build when a build script exists.
| Dependency in package.json | Detected as | Proposed start command |
|---|---|---|
next |
Next.js | start script, else npx next start |
nuxt |
Nuxt | node .output/server/index.mjs |
@remix-run/serve, @react-router/serve, @remix-run/node |
Remix / React Router | start script, else npx remix-serve build/index.js |
@sveltejs/kit with adapter-node |
SvelteKit | node build |
astro with @astrojs/node |
Astro | node ./dist/server/entry.mjs |
@nestjs/core |
NestJS | npm run start:prod if that script exists, else node dist/main |
express, fastify, koa, hono, or anything else |
Express / Fastify / Koa / Hono / Node.js | start script, else node <main>, node server.js, node index.js or node app.js |
Next.js with output: 'export', Astro without @astrojs/node, SvelteKit with adapter-static, and Vite projects without a start script are deployed as static sites. A Procfile with a web: line always provides the start command.
Click Analyze repository on the Deploy tab to see the result before deploying, then Use these settings and customize to edit it. Notes such as "Could not find a start script; set the start command manually" appear under the settings.
Deploy an app
- Open Websites → New website, choose Git deploy, enter the Repository URL, Branch and, for monorepos, the Root directory, then click Create website.
- Wait for the first deployment in the task log. ZoPanel clones the code, runs the install and build commands, starts the app and switches traffic to it once it is healthy.
- To change commands, open the Deploy tab, turn Auto-detect off in Build & run, edit the fields and click Save & deploy.
To deploy without Git, choose Uploaded files (File Manager) under Source, upload the project to domains/<domain>/source and click Deploy.
PORT and HOST
The app must listen on 127.0.0.1 at the port in the PORT environment variable. ZoPanel sets PORT (different for each blue/green slot) and HOST=127.0.0.1; you cannot override them.
// Express
const port = process.env.PORT || 3000;
app.listen(port, "127.0.0.1");
// NestJS: main.ts
await app.listen(process.env.PORT ?? 3000, "127.0.0.1");
next start, Nuxt's server output, SvelteKit's adapter-node and Astro's @astrojs/node read PORT on their own. Never hard-code a port.
Environment variables
Add variables in the Environment variables card, or click Paste .env to paste many at once. Then click Save & redeploy.
- Up to 200 variables. Names use letters, digits and
_and cannot start with a digit; values are a single line of up to 8,192 characters. - Variables are available to the install and build commands and at runtime. Members with view-only access see the names, not the values.
NODE_ENV=productionis set for the running app only. During the build it is not set, sonpm cistill installsdevDependencies(TypeScript, Vite…). Important: if you addNODE_ENV=productionyourself, it also applies to the build and npm skipsdevDependencies, which usually breaks the build.CI=trueis set during the build.
Persistent paths
Each deployment builds into a new folder under domains/<domain>/releases/, so files the app writes there disappear with the next release. List folders or files to keep in Persistent paths, separated by commas, for example uploads, .env.
- They are stored in
domains/<domain>/shared/and linked into every release. On the first deploy, content already in the repository at that path is copied there. - Names starting with
.envare treated as files, everything else as folders. - Important: when
.envis a persistent path and the website has environment variables, ZoPanel rewritesshared/.envfrom those variables on every deploy. Keep values in one place.
Cluster mode (every CPU core)
A Node.js process uses one CPU core. The Use every CPU core card (Node.js server apps only) starts one worker per CPU the package allows, up to 16, without code changes: ZoPanel preloads a small helper with Node's cluster module, and the workers share the port.
- Make sure the app keeps no state in memory: sessions, caches, rate-limit counters and websocket rooms must live in Redis or a database.
- Turn on One process per CPU core (redeploys to apply). Changing the switch starts a deployment.
A crashed worker is restarted after one second; after more than 20 quick crashes the app stops with [zopanel] workers keep crashing, stopping in the output. To use fewer workers, set ZP_CLUSTER_WORKERS in the environment variables.
Zero-downtime deploys and rollback
Every server app has two slots. A new release starts in the idle slot on its own port and must answer the Health check path (default /) with an HTTP status below 500 within 90 seconds. Only then does nginx switch to it; the previous process is stopped a few seconds later, so requests in flight finish. A release that fails to build, exits or never becomes healthy is discarded and the live one keeps running.
The Releases card keeps the last 5 builds. Rollback starts the chosen release in the idle slot with the current settings and switches after the health check, without downtime. It does not rebuild.
Note: during a switch both releases run at the same time for a few seconds, which also doubles memory use for that moment.
Logs
- Application output on the Deploy tab shows the last 400 lines of stdout and stderr from both slots. Turn on Live to follow it.
- The build log of each deployment is in Tasks, or click View log in Deployments.
- The website's Logs tab shows nginx's access and error logs.
Memory and CPU limits
The app runs inside the hosting account's resource group, so the package's RAM, CPU and process limits apply to all its websites together. The status card shows the app's current RAM. If the account runs out of memory, the kernel stops the process and systemd restarts it after one second.
To give V8 a heap limit below the package's memory, add an environment variable:
NODE_OPTIONS=--max-old-space-size=384
WebSockets
nginx forwards the Upgrade and Connection headers, so WebSockets and Server-Sent Events work without extra settings. Proxy buffering is off and a connection idle for 300 seconds is closed, so send a ping or heartbeat more often than that. With cluster mode on, use a shared adapter (for example Socket.IO's Redis adapter) and the WebSocket transport, because each worker holds its own connections.
Troubleshooting
| Message or symptom | Cause and fix |
|---|---|
the app did not respond on port … within 90s (it must listen on $PORT) |
The app listens on a fixed port or crashed before listening. Read process.env.PORT. The last 25 lines of output are printed in the task log. |
the app exited during startup |
Open Application output: usually a missing environment variable, a missing build (.next not found) or a module that failed to load. |
Node.js is not installed — install it under Runtimes |
No Node.js version on the server. An administrator installs one under Runtimes. |
install failed: … / build failed: … |
The command failed; the task log shows its output. Check that the lockfile matches package.json (npm ci refuses a mismatch). |
a start command is required for server apps |
Set Start command, or switch Type to Static site for a front-end build. |
start command must be a single line (chain commands with &&) |
Commands are one line, up to 2,000 characters. |
port … is already in use by another process (uid …); try again in a few seconds |
The previous process is still stopping. Deploy again after a few seconds. |
a deployment of this app is already running |
Wait for the current deployment to finish. |
| 502 or the busy page after a deploy | The process stopped later (for example out of memory). Check Application output and the account's memory under Resources. |
| Build is killed or very slow | Builds share the package's CPU and memory. Raise the package's RAM, or reduce the build's memory with NODE_OPTIONS. |
Related
- Git deploy
- Node.js and Python apps
- Static sites and front-end builds
- Packages and limits
- Next.js deployment docs
- Node.js release schedule