How to install a Node server on Virtualmin

Background

Virtualmin, like cPanel, is designed for PHP/MySQL shared hosting. But what if you want to run NodeJS on it? Surely JavaScript is pretty simple and a flexible platform like Virtualmin should just support it?

The bottom line is building an automation platform for PHP/MySQL and Apache and Nginx isn’t the same as building a NodeJS server. So no, Virtualmin (nor cPanel), properly supports it. However, with a bit of elbow grease you can get it working. Here are some tips to get you started:

Setup

Install Virtualmin as usual

SSH as root to the server

Install the latest LTS version of Node (e.g. 24). By default, servers don ‘t have it on.

curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -

Open the firewall for the Node server port.

One has to decide on which port NodeJS is going to run. For this tutorial, we’re settling on port 3000.

You probably don’t want Node’s server port running on the Internet,  but, at the same time, it’s really useful for testing from the outside.

Navigate to Webmin/Networking/FirewallD and add TCP port 3000.

Next, login as the user. Decide what is going to be your application directory. For this example, we have chosen ~/app

Do this (as the user, and not as root):

mkdir ~/app
cd ~/app
npm init -y

Now create a minimal file called server.js to show that NodeJS is running:

const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello world!');
});
server.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Next, run the server:

node server.js

Next, visit the HTTP (note, not SSL) URL on your server:

http://example.com:3000

If all went well, you should now see:

Hello world!

So far, so simple?

There are still a few things to do:

  • SSL, including automatic renewal
  • Persistence of the server (including automatic restart)

SSL

SSL is a lot easier than expected. Simply follow this screenshot and then after you’ve made the change, “Re-check server configuration”.

To keep things tidy from the outside, also change your node server to *only* listen on localhost:

server.listen(3000, '127.0.0.1', () => {
  console.log('Server running on http://127.0.0.1:3000');
});

and then

pm2 restart nodejs-node-app

Now that node is only running on localhost, you may also remove it from the firewall allow list.

Persisting the server

The NodeJS server needs to run all the time. At times, the user might make code changes, meaning the NodeJS server has to be restarted.

The basics of background processes in Linux can be demonstrated below.

  • First, the node server is start with & to send it to the background. The server replies with the process ID and a message about the server running.
  • Next, we use ps -ef | grep server to see our work.
  • Finally, we can manually terminate the process by kill -9
user@host:~/app$ node server.js &
[1] 23599
user@host:~/app$ Server running on http://localhost:3000

user@host:~/app$
user@host:~/app$ ps -ef | grep server
root 7773 1 0 10:22 ? 00:00:00 /usr/share/webmin/virtual-server/lookup-domain-daemon.pl
nodejs 23599 18889 0 11:18 pts/4 00:00:00 node server.js
nodejs 23609 18889 0 11:19 pts/4 00:00:00 grep server
user@host:~/app$ kill -9 25399

Of course, all the manual intervention needs to be avoid so instead, we’ll use PM2.

PM2

The next steps are to be performed by root:

npm install -g pm2
pm2 start /home/user/app/server.js --name user-node-app
pm2 save
pm2 startup

To see if it’s working, do this:

pm2 logs nodejs-node-app

PM2 Debug Output

root@host:~# pm2 start /home/nodejs/app/server.js --name nodejs-node-app

-------------

__/\\\\\\\\\\\\\____/\\\\____________/\\\\____/\\\\\\\\\_____
_\/\\\/////////\\\_\/\\\\\\________/\\\\\\__/\\\///////\\\___
_\/\\\_______\/\\\_\/\\\//\\\____/\\\//\\\_\///______\//\\\__
_\/\\\\\\\\\\\\\/__\/\\\\///\\\/\\\/_\/\\\___________/\\\/___
_\/\\\/////////____\/\\\__\///\\\/___\/\\\________/\\\//_____
_\/\\\_____________\/\\\____\///_____\/\\\_____/\\\//________
_\/\\\_____________\/\\\_____________\/\\\___/\\\/___________
_\/\\\_____________\/\\\_____________\/\\\__/\\\\\\\\\\\\\\\_
_\///______________\///______________\///__\///////////////__


Runtime Edition

PM2 is a Production Process Manager for Node.js applications
with a built-in Load Balancer.

Start and Daemonize any application:
$ pm2 start app.js

Load Balance 4 instances of api.js:
$ pm2 start api.js -i 4

Monitor in production:
$ pm2 monitor

Make pm2 auto-boot at server restart:
$ pm2 startup

To go further checkout:
http://pm2.io/


-------------

[PM2] Spawning PM2 daemon with pm2_home=/root/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /home/nodejs/app/server.js in fork_mode (1 instance)
[PM2] Done.
┌────┬────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ nodejs-node-app │ default │ 1.0.0 │ fork │ 33651 │ 0s │ 0 │ online │ 0% │ 30.3mb │ root │ disabled │
└────┴────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
root@host:~# pm2 save
[PM2] Saving current process list...
[PM2] Successfully saved in /root/.pm2/dump.pm2
root@host4:~# pm2 startup
[PM2] Init System found: systemd
Platform systemd
Template
[Unit]
Description=PM2 process manager
Documentation=https://pm2.keymetrics.io/
After=network.target

[Service]
Type=forking
User=root
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
Environment=PM2_HOME=/root/.pm2
PIDFile=/root/.pm2/pm2.pid
Restart=on-failure

ExecStart=/usr/lib/node_modules/pm2/bin/pm2 resurrect
ExecReload=/usr/lib/node_modules/pm2/bin/pm2 reload all
ExecStop=/usr/lib/node_modules/pm2/bin/pm2 kill

[Install]
WantedBy=multi-user.target

Target path
/etc/systemd/system/pm2-root.service
Command list
[ 'systemctl enable pm2-root' ]
[PM2] Writing init configuration in /etc/systemd/system/pm2-root.service
[PM2] Making script booting at startup...
[PM2] [-] Executing: systemctl enable pm2-root...
Created symlink '/etc/systemd/system/multi-user.target.wants/pm2-root.service' → '/etc/systemd/system/pm2-root.service'.
[PM2] [v] Command successfully executed.
+---------------------------------------+
[PM2] Freeze a process list on reboot via:
$ pm2 save

[PM2] Remove init script via:
$ pm2 unstartup systemd

Tags

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to Top