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
deployas 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
- Log in to CloudPanel as an administrator.
- Open Sites and choose Add Site.
- Select the Node.js site type.
- 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. - Fill in the App Port field — the port your process will listen on, for example
3000. - 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:
ssh deploy@203.0.113.10
cd /home/deploy/htdocs/example.com
git clone https://github.com/example/app.git .
npm ci --omit=devIf your project needs a build step, run it now:
npm run buildThen 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:
which node && node -vNote 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:
[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.targetReplace 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:
sudo systemctl daemon-reload
sudo systemctl enable --now example-node
sudo systemctl status example-nodeA 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:
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/.envWarning: .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:
sudo cat /etc/nginx/sites-enabled/example.com.confIf your app uses WebSockets, or needs the real client IP and scheme, add these lines inside the location block:
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:
sudo nginx -t && sudo systemctl reload nginxWarning: 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:
curl -I http://127.0.0.1:3000
curl -I https://example.com
sudo journalctl -u example-node -n 50 --no-pagerYou 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-nodeandss -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 badDATABASE_URLandEADDRINUSEare the usual causes. - Assets return 404 after a build — the
WorkingDirectoryis wrong, or the build output folder differs from what the app serves. Confirm the folder names inpackage.jsonand 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
- Deploy and supervise the same app with PM2: Deploy a Node.js app with PM2 behind Nginx.
- Understand what the generated vhost is
