Control panels

CloudPanel: host a Node.js app with the Node.js site type

Add a Node.js site in CloudPanel, point it at your app port, run the process under systemd and let the generated Nginx vhost serve it on your domain.

IntermediateLinux server6 min read

To host a Node.js app on CloudPanel, add a site with the Node.js site type, give it an app port, and run your process on that port — CloudPanel writes an Nginx vhost that proxies your domain to it. This guide walks through the site type's settings, the start command, environment variables, and what to check when the proxy answers with 502.

Before you start

  • A Linux VPS with CloudPanel installed, and a domain whose DNS points at the server (203.0.113.10 in the examples below).
  • SSH access as root or as the site user CloudPanel created for the domain. The examples use deploy as that user.
  • Your app code or a git repository URL. Apps that listen on a TCP port work best here: Express, Fastify, Nest, or a Next.js server build.
  • A free port for the app, for example 3000. Nginx will proxy to it over localhost.
  • Server power controls, the root password, OS reinstall and a browser console are all in your VPS provider's control panel if you need to get back in.

Warning: don't open the app port in the firewall. Nginx talks to it over localhost, so it never needs to be reachable from the internet.

Create a CloudPanel Node.js app site

  1. Log in to CloudPanel as an administrator.
  2. Open Sites and choose Add Site.
  3. Select the Node.js site type.
  4. Enter the domain (example.com), keep the suggested site user or set your own, and pick the Node.js version you want for this site.
  5. Fill in the App Port field — the port your process will listen on, for example 3000.
  6. Create the site.

CloudPanel creates the site directory at /home/deploy/htdocs/example.com, provisions the site user, and writes an Nginx vhost that proxies requests to http://127.0.0.1:3000. It does not start or supervise your Node process — that is the part you handle below. CloudPanel's clpctl CLI can create sites as well; check the vendor's documentation for the current command flags, since they change between releases.

Upload the app and install dependencies

Connect as the site user and put the code in the site directory:

Bash
ssh deploy@203.0.113.10
cd /home/deploy/htdocs/example.com
git clone https://github.com/example/app.git .
npm ci --omit=dev

If your project needs a build step, run it now:

Bash
npm run build

Then confirm which Node binary this site uses, because CloudPanel keeps Node.js versions per site and your shell may not point at the same one the panel selected:

Bash
which node && node -v

Note the full path it prints. You need it for the systemd unit in the next section.

Run the app on the app port

CloudPanel does not supervise the Node process, so run it under systemd. As root, create /etc/systemd/system/example-node.service:

INI
[Unit]
Description=Node.js app for example.com
After=network.target

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/home/deploy/htdocs/example.com
EnvironmentFile=/home/deploy/htdocs/example.com/.env
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStart=/usr/local/bin/node /home/deploy/htdocs/example.com/server.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Replace the ExecStart binary with the path you got from which node, and adjust the entry file (server.js, index.js, dist/main.js) to match your project. Then load and start it:

Bash
sudo systemctl daemon-reload
sudo systemctl enable --now example-node
sudo systemctl status example-node

A healthy unit shows active (running). If it exits with status=203/EXEC, the Node path in ExecStart is wrong. If it exits with EADDRINUSE, another process already holds port 3000.

Tip: PM2 works too, and gives you cluster mode and log rotation. If you prefer it, see Deploy a Node.js app with PM2 behind Nginx. Run only one supervisor per app — not both systemd and PM2.

Set environment variables

Keep secrets out of the code. Create an .env file in the site root and read it from your app (dotenv, or the framework's built-in loader) or from the systemd EnvironmentFile shown above:

Bash
cat > /home/deploy/htdocs/example.com/.env <<'EOF'
NODE_ENV=production
PORT=3000
DATABASE_URL=mysql://appuser:password@127.0.0.1:3306/appdb
EOF
chmod 600 /home/deploy/htdocs/example.com/.env

Warning: .env holds credentials. Keep it at mode 600, owned by the site user, and never commit it to git. Restart the service after any change with sudo systemctl restart example-node — the app reads these values only at startup.

If your app needs a database, create the database and user first — the steps are in How to create a database and user for a web app in MySQL or MariaDB.

Reverse proxy and HTTPS

The vhost CloudPanel generated already proxies your domain to the app port. Read it to see the exact configuration:

Bash
sudo cat /etc/nginx/sites-enabled/example.com.conf

If your app uses WebSockets, or needs the real client IP and scheme, add these lines inside the location block:

Nginx
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

Test and reload before you walk away:

Bash
sudo nginx -t && sudo systemctl reload nginx

Warning: CloudPanel can regenerate the vhost when you change site settings, and that overwrites manual edits. Keep custom directives in a snippet file and include it, so a panel change doesn't silently drop your WebSocket headers.

Then issue a certificate from the site's SSL/TLS tab so the domain serves HTTPS.

Check it works

Test the app directly, then through the proxy and the public domain:

Bash
curl -I http://127.0.0.1:3000
curl -I https://example.com
sudo journalctl -u example-node -n 50 --no-pager

You want a 200 (or your app's real status) on both curls, and request lines appearing in the journal when you load the page in a browser. Check that static assets load and that a POST or login action reaches the app, not just the homepage.

Troubleshooting

  • 502 Bad Gateway — Nginx reached localhost but nothing answered. Run systemctl status example-node and ss -lntp | grep 3000. Usually the app crashed at boot, or it listens on a different port than the one you entered in the panel.
  • Service exits right after start — run it in the foreground as the site user to see the real error: sudo -u deploy node /home/deploy/htdocs/example.com/server.js. Missing environment variables, a bad DATABASE_URL and EADDRINUSE are the usual causes.
  • Assets return 404 after a build — the WorkingDirectory is wrong, or the build output folder differs from what the app serves. Confirm the folder names in package.json and the service file.
  • WebSockets disconnect every few seconds — the proxy upgrade headers are missing. Add the snippet above, run nginx -t, then reload.
  • Permission denied on files or logs — files are owned by root after an upload as root. Fix with sudo chown -R deploy:deploy /home/deploy/htdocs/example.com.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.