Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Running Node.js on CloudPanel: The Parts the Docs Assume You Know

Sean

Platform Writer

Aug 14, 2026
9 min read

CloudPanel does not run your Node application. It gives you an nginx virtual host, a Linux user, and a place to point a domain, and expects you to supply the process that listens on a port and the supervisor that keeps it alive. Once that split is clear the setup is straightforward, and most of the confusion comes from not knowing which half owns what.

Running Node.js on CloudPanel: The Parts the Docs Assume You Know

The official documentation covers the mechanics accurately but assumes familiarity with the surrounding pieces. This fills in the assumptions: what the reverse proxy is doing, why your app must bind where it does, and the operational parts nobody mentions until they bite.

Table of contents

The architecture in one paragraph

CloudPanel creates a site with its own system user and home directory, and configures nginx to listen on 80 and 443 for your domain, terminate TLS, and forward requests to a local port. Your Node process listens on that port. That is the entire arrangement.

Three consequences follow, and each one is a bug people file.

  • Your app must listen on the port CloudPanel is proxying to, and nothing else. Hardcoding 3000 when the vhost forwards to 8080 produces a 502 with a healthy-looking process.
  • Bind to 127.0.0.1 rather than 0.0.0.0. The proxy reaches it locally, and binding to all interfaces exposes the app directly on that port, bypassing TLS.
  • Node never sees the certificate. nginx terminates TLS, so req.protocol reports http unless you trust the proxy headers.

That last one is behind a large share of redirect loops. Your app checks the protocol, sees http, redirects to https, nginx forwards the new request as http again, and around it goes.

// Express: trust the proxy so req.protocol and req.ip reflect
// the X-Forwarded-* headers nginx sets rather than the local hop.
app.set("trust proxy", 1);

const port = process.env.PORT || 3000;
app.listen(port, "127.0.0.1", () => {
  console.log(`listening on 127.0.0.1:${port}`);
});

Keeping the process alive

Starting the app over SSH runs it until your session ends. You need a supervisor, and there are two reasonable choices.

PM2 is the common one and works well. Install it for the site user, start the app, save the process list, and generate the startup hook so it survives a reboot.

# As the site user, from the app directory.
npm install -g pm2
pm2 start npm --name my-app -- start

# Persist the list, then install the boot hook.
pm2 save
pm2 startup
# Run the sudo command it prints, then pm2 save again.

The step people skip is pm2 startup. Without it everything works perfectly until the server reboots, at which point the site is down and nothing in the logs explains why.

The alternative is a systemd unit, which has no extra dependency and integrates with journalctl for logs. On a server you already administer this is arguably the cleaner option.

[Unit]
Description=my-app
After=network.target

[Service]
Type=simple
User=my-site-user
WorkingDirectory=/home/my-site-user/htdocs/example.com
EnvironmentFile=/home/my-site-user/.env
ExecStart=/home/my-site-user/.nvm/versions/node/v22.11.0/bin/node server.js
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Use the absolute path to the Node binary. systemd does not read your shell profile, so an nvm-managed node is not on its PATH and the unit fails with a file-not-found that reads like the script is missing.

The Node version trap

This one costs people an afternoon. CloudPanel installs Node per site user through nvm, which means the Node on your interactive shell PATH comes from a line in that user’s profile.

Cron jobs and systemd units do not source that profile. So the app runs when you start it by hand and fails from cron with an obscure syntax error, which is actually an old system Node choking on modern syntax.

Always use absolute paths in anything non-interactive. Find the real path once and use it everywhere.

# The path to use in cron entries and unit files.
which node
# /home/my-site-user/.nvm/versions/node/v22.11.0/bin/node

# In a crontab, set PATH explicitly at the top.
PATH=/home/my-site-user/.nvm/versions/node/v22.11.0/bin:/usr/local/bin:/usr/bin:/bin
0 3 * * * cd /home/my-site-user/htdocs/example.com && node scripts/nightly.js

Note that upgrading Node changes the path. Anything holding the old absolute path breaks silently at the next version bump, which is a good argument for a symlink you control or an environment file you update in one place.

WebSockets, timeouts, and the deploy gap

Two nginx defaults will surprise you. WebSocket upgrades need explicit header handling in the location block or the connection downgrades to plain HTTP and your realtime features silently stop working. And the default proxy read timeout will cut off any request that takes longer than about a minute, which matters for uploads and long report generation.

Both are edits to the site’s vhost configuration, which CloudPanel exposes for you to modify.

The larger gap is deployment. CloudPanel gives you a server and a panel, not a pipeline. Shipping a change means pulling the repository, installing dependencies, running the build, and restarting the process, in that order, and the site is inconsistent while it happens.

You can script that, and you should. But it is worth being clear about what you have not got: there is no build log tied to a commit, no previous version to roll back to, and no record of which change is running right now beyond whatever git reports. When a deploy breaks production at an unhelpful hour, the recovery is to fix forward under pressure.

That is the real trade with a control panel. You get full control of the box and you own every part of the release process, including the parts you only think about the first time one goes wrong.

Working checklist

  1. Bind the app to 127.0.0.1 on the exact port the vhost proxies to.
  2. Set trust proxy so protocol and client IP are read from the forwarded headers.
  3. Put the app under PM2 or systemd, and confirm it comes back after a real reboot.
  4. Use absolute Node paths in cron and unit files, never the bare command.
  5. Add the WebSocket upgrade headers to the vhost if the app uses them.
  6. Raise the proxy read timeout if any request legitimately runs long.
  7. Keep environment variables in a file outside the repository, referenced by the supervisor.

Step three is the one worth actually testing rather than assuming. Reboot the server deliberately, once, while you are watching.

How this fits the rest of the stack

A control panel is a good fit when you want the server and are willing to own the release process. When the interesting problem is the application rather than the machine, the same Node app deployed from a GitHub repository gets a build log attached to each commit, a live route, environment variables in the dashboard, and a rollback to the previous deploy when a release goes wrong. Costing that against a VPS plus your own time is a fair comparison to run, and the RunxBuild hosting calculator shows the service, database, and storage as separate line items.

Useful related references:

FAQ

Why does my Node app show a 502 on CloudPanel?

Almost always the proxy target does not match where the app is listening. Confirm the port in the site’s nginx configuration matches the port the process actually bound to, and that the process is running at all.

Should Node bind to 0.0.0.0 or 127.0.0.1 behind CloudPanel?

127.0.0.1. The reverse proxy connects locally, and binding to all interfaces exposes the app directly on that port, letting clients bypass TLS termination entirely.

How do I keep a Node app running after logout on CloudPanel?

Use PM2 or a systemd unit. With PM2, run pm2 save and pm2 startup so the process list is restored on boot. Skipping the startup step means the app dies at the next reboot with nothing explaining it.

Why does my cron job fail with a Node syntax error?

Cron does not source the shell profile where nvm sets its PATH, so it finds an older system Node. Use the absolute path to the Node binary, or set PATH explicitly at the top of the crontab.

Do WebSockets work on CloudPanel?

Yes, but only after adding the upgrade headers to the location block in the site’s nginx configuration. Without them the connection downgrades to plain HTTP and realtime features stop working with no obvious error.

#CloudPanel#Node.js#Reverse Proxy#PM2#Server Management