Node.js
The Node.js handler runs your app (Express, Fastify, Next.js, Nuxt, an API server or anything else that listens on a port) and passes the site’s requests to it. WardPane provides the Node.js runtime, the PM2 process manager, a systemd sandbox, HTTPS, the protection checks and static file serving.
How it fits together
visitor -> WardPane on 80/443 -> protection checks -> file in public/? -> served directly
-> otherwise -> your app on 127.0.0.1:<port>
- Each Node.js site gets its own port, which the panel assigns from the range 10000 to 19999. Your app listens on it on
127.0.0.1, so nobody can reach the app from outside except through WardPane. - Apps run under PM2. Each owner has one PM2 daemon in the systemd unit
wardpane-pm2@<owner>.service, and every Node.js site of that owner is one PM2 app in it. - The app runs as the site’s owner (by default
www-root), inside a systemd sandbox.
Folder layout
The site folder is the app folder. Put package.json there, next to a public/ folder:
/var/www/www-root/data/www/example.com/
package.json
package-lock.json
server.js
node_modules/
public/ served directly as files
favicon.ico
css/app.css
WardPane serves files directly only from public/. Your code, node_modules and .env files stay in the site folder where no visitor can download them.
For a site from before 2.0 the app folder is /var/www/<domain>, with package.json next to its public/.
Setting up an app
- Create the site with the Node.js handler (or switch an existing site on its Handler tab). In the create dialog you can pick the Node.js version right away.
- Upload the app into the site folder over SFTP or with
git clonefrom the shell. - On the Handler tab, choose how the app starts and add environment variables.
- Press Save changes. The panel assigns a port, downloads Node.js if needed and starts the app.
- Press npm install to install the dependencies, then Restart.
The “Node.js app” card at the top of the Handler tab shows the state of the app (running, stopped, failed, starting), the exact Node.js version and the port.
Settings
You will find all of these on the site’s Handler tab.
Node.js version
Choose a Node.js major. WardPane 2.0 offers 20, 22, 24 and 26; new sites get 24. Node.js 20 reached its end of support in April 2026, so use 22 or newer for new apps.
The newest release of the chosen line is downloaded the first time a site needs it, see Runtimes. The line is set per site, so two apps of the same owner can run on different Node.js versions.
Start command
There are two ways to start the app:
- npm script: a script from
package.json, for examplestart. Press Read package.json to list the scripts of the app and pick one. - Entry file: a JavaScript file that starts the server, for example
server.js, relative to the site folder..js,.mjsand.cjsfiles work.
Environment variables
Add up to 20 variables. Names are letters, digits and _, starting with a letter or _; values can be up to 4096 characters.
The panel sets some variables itself:
| Variable | Value |
|---|---|
PORT |
The port the panel assigned to the site. Your app must listen on it. |
HOST |
127.0.0.1. Your app must listen on this address. |
NODE_ENV |
production, unless you set another value. |
PORT, HOST, PATH, HOME and PM2_HOME cannot be overridden. The panel stores the values as plain text, and anyone who can open the site’s settings can read them.
Port
The panel assigns the port when the site first becomes a Node.js site. No other site gets the same port, and it never changes, even if you switch the site to another handler and back. The field is read-only.
Serve files from public/ directly
On by default. Files that exist in public/ are sent by WardPane itself (with ETags and resumable downloads, like on a static site), and everything else goes to the app. Turn it off if the app should see every request, including those for static files.
Limits
| Setting | Default | Range |
|---|---|---|
| Memory limit, MB | 512 | 64 to 16384 |
| CPU limit, % | 100 (one core) | 10 to 800 |
When the app grows above its memory limit, PM2 restarts it. The owner’s PM2 unit gets the sum of its apps’ limits (plus a little for PM2 itself) as its systemd memory limit and CPU quota.
Block outgoing connections
With this on, the app can only talk to this server. Calls to external APIs, sending mail and downloads all fail. All Node.js apps of one owner share one PM2 daemon, so the block takes effect only when every Node.js app of that owner has it on.
PM2 from the shell
PM2 keeps its state in the owner’s home (/var/www/<owner>/data/.pm2), so the usual commands work from that owner’s shell, for example in the panel’s shell client:
pm2 list # the owner's apps, named after their domains
pm2 restart example.com # restart one app
pm2 logs example.com # follow its output
Change the version, start command, environment and limits in the panel. The panel regenerates the PM2 configuration from its own settings and would overwrite changes made by hand.
The proxy
Requests that are not served from public/ go to http://127.0.0.1:<port>:
- They pass every protection check of the site first (Bot Fight, Geo, rate limits, required URL parameters, the site password), at the same point where a PHP site hands the request to PHP.
- Bodies are streamed in both directions.
X-Forwarded-For,X-Real-IP,X-Forwarded-ProtoandX-Forwarded-Hostcarry the real visitor address, scheme and host, also behind Cloudflare. Whatever the client itself sends in these headers (and inForwarded) is dropped. In Express, trust the local proxy withapp.set('trust proxy', 'loopback')to get the visitor IP inreq.ip.- WebSockets work. An upgrade request goes through the same protection checks as any other request before it reaches the app.
- Hop-by-hop headers are removed, and
X-Powered-Byis dropped from the app’s responses. - If the app is down or does not listen on its port, visitors get a plain 502 Bad Gateway page with no details about the server. If the app does not start answering in 2 minutes, the answer is 504. The panel log gets the reason, at most once a minute per site.
- Request bodies up to 100 MB are passed to the app; larger ones get 413.
Restart and npm install
The “Node.js app” card has two buttons:
- Restart restarts the app in PM2.
- npm install installs the dependencies:
npm ciif the folder has apackage-lock.json, otherwisenpm install. It runs as the owner, in a sandbox, with the site’s own Node.js version, and is stopped after 10 minutes. The output goes to thenpm-install.logof the site.
Logs
The Logs card on the Handler tab shows three files: Output (node-out.log), Errors (node-error.log) and npm install (npm-install.log).
| Layout | Where the logs are |
|---|---|
| New sites (2.0) | /var/www/<owner>/data/logs/<domain>-node-out.log, <domain>-node-error.log, <domain>-npm-install.log |
| Sites from before 2.0 | /var/www/<domain>/logs/node-out.log, node-error.log, npm-install.log |
Runtimes and security
- Node.js builds are downloaded from
https://nodejs.org/dist/(linux-x64 or linux-arm64) and checked against the publishedSHASUMS256.txtbefore they are unpacked. They live in/opt/wardpane-runtimes. When no site uses a version any more, it is removed. install.shinstalls a pinned version of PM2.- Code from a runtime never runs as root. The root helper only downloads and unpacks it; the apps and
npmrun as the owner. - The PM2 unit runs in a systemd sandbox: the system is read-only, only the owner’s folders are writable, no new privileges, a private
/tmp.
Deploying an Express app
-
Create the site
api.example.comwith the Node.js handler and version 24. Point its DNS to the server first, so the certificate is issued right away. -
In the app, listen on the port and address the panel gives:
const express = require('express'); const app = express(); app.set('trust proxy', 'loopback'); app.get('/api/hello', (req, res) => res.json({ hello: req.ip })); app.listen(process.env.PORT, process.env.HOST); -
Add a start script to
package.json:{ "name": "api", "scripts": { "start": "node server.js" }, "dependencies": { "express": "^4.21.0" } } -
Upload
package.json,package-lock.json,server.jsand apublic/folder into the site folder (for the default owner:/var/www/www-root/data/www/api.example.com). -
On the Handler tab, choose npm script and
start(or press Read package.json and pick it), then Save changes. -
Press npm install and wait until it finishes (watch the npm install log).
-
Press Restart. The status card should say the app is running; open
https://api.example.com/api/hello.
If the site shows 502, look at the Errors log. Most often a dependency is missing, the start script has a typo, or the app listens on a fixed port instead of PORT.