Deploy a React Application with Node.js on CentOS

I was interested in knowing how to deploy a Node.js server that can serve a React application without resorting to Heroku or other hosting services, so I searched for a few tutorials from which I drew the steps needed to deploy on my own VPS. If you are interested as well and your environment is similar to mine, then this tutorial could be what you’re looking for.

My Environment #

Operating system: CentOS Linux 7.8.2003 on x86_64
Web server: Apache HTTP Server 2.4.6
Root access: Yes (you will need root access for this tutorial)
Node.js 14.15.5 with nodemon
npm: 6.14.11 (npx is not needed but I also have version 6.14.11)
React 17.0.1 with Express

Aside from Node.js, npm, and React, only one npm package needs to be installed to keep the Node.js server running as a service automatically across operating system reboots, and that is the process manager called pm2, which at the time of this tutorial is version 4.5.4.

An Overview Before We Begin #

We’ll cover the installation of just Node.js (which also installs npm and npx) and pm2. You can use your own React application or simply create a new one with the create-react-app command.

A typical Node.js and React setup has the React application running on some port and making API requests to Node.js running on a different port. In many instances, this is achieved by having React itself proxy those requests.

In this tutorial, we will proxy instead by inserting directives into the httpd.conf file of the Apache HTTP Server. We will do so in a way that is compatible with React client-side routing.

Finally, we will also configure pm2 to keep your Node.js server running forever!

Upload React Application #

Before we start, you would need to know the URLs that your React application uses to make API calls to your Node.js server. If the URLs contain a port, then they should be changed to exclude the port in preparation for the proxy rules that we will apply to the Apache HTTP Server. As an example, if the URLs look like https://yourdomain.com:5000/api/login, then they should be changed to https://yourdomain.com/api/login to allow Apache to catch the API requests at the default HTTP port (80 for http and 443 for https) and proxy them to the Node.js server.

Following advice from React (https://create-react-app.dev/docs/deployment/#building-for-relative-paths), make sure package.json contains the “homepage” line like so:

"homepage": "https://yourdomain.com",

Before you can upload, build your React application with

npm run build

Upload the created files in the “build” folder to the public_html folder of your domain.

Install Node.js #

Node.js can be installed with a few simple commands. I installed it under /usr/local/lib/nodejs, but you can modify the commands to install a different version of Node.js to a different location. Run this as a user that has permissions to /usr/local/lib or wherever your preferred location is:

cd /usr/local/lib
mkdir nodejs
cd nodejs
wget https://nodejs.org/download/release/v14.15.5/node-v14.15.5-linux-x64.tar.xz
tar -xJvf node-v14.15.5-linux-x64.tar.xz -C .

Open your .bash_profile to add Node.js to your environment path. If you’re not comfortable with vim, you’ll have to edit it in another way.

vim ~/.bash_profile

Append the path of the Node.js bin folder to the PATH environment variable. For example, if PATH looks like this:

PATH=$PATH:$HOME/bin

It should be changed to (notice I colored what we need to add in blue):

PATH=$PATH:$HOME/bin:/usr/local/lib/nodejs/node-v14.15.5-linux-x64/bin

Save the file, then refresh your terminal with the modified bash profile (“source ~/.bash_profile” would also work):

. ~/.bash_profile

Now running “node -v”, “npm version”, and “npx -v” should all work.

Configure Node.js #

The steps to configure Node.js should be performed as the user that owns the public_html folder of the domain. If, for example, the owner of /home/joe/domain.com/public_html is joe, then make sure to log in as joe.

If this user does not have the Node.js path in .bash_profile, you can follow the steps above to update it.

On your host server, create a folder named “nodejs” in the parent directory of public_html. This means if you have /home/joe/domain.com/public_html, the “nodejs” folder should be at /home/joe/domain.com/nodejs. Upload your Node.js server files to the “nodejs” folder. Note that we avoided putting source code directly in public_html in order to reduce the risk of exposing it to the public.

In your terminal, go to the created “nodejs” folder and add the “path” module to package.json as well as install all the other modules listed in there:

npm i path
npm i

Edit index.js to configure Node.js to serve the React app’s index.html as a fallback if none of the API routes are caught by Express. Your index.js most likely has your own code in it, so be mindful not to just copy and paste the code below, but read the comments to guide your modifications.

import { fileURLToPath } from 'url';
import { dirname } from 'path';
import path from 'path';

// For details on the usage of dirname, see https://stackoverflow.com/questions/64383909/dirname-is-not-defined-in-node-14-version
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// All of your other routes goes here

// Serve the React app's index.html as a fallback if none of the API routes were caught by Express
// Because NodeJS runs from the "nodejs" folder, .. is needed to move one directory up to properly reach public_html folder
app.use(express.static(path.join(__dirname, '..', 'public_html')));
app.get('/*', function (req, res) {
    res.sendFile(path.join(__dirname, '..', 'public_html', 'index.html'));
});

If all went well, you should be able to start Node.js by running “npm start” in the “nodejs” folder.

Configure Proxy Rules for Apache HTTP Server #

Now that the React application has been uploaded to public_html and Node.js was able to start, it’s time to add proxy rules so that the Apache server can take incoming requests and proxy them to whichever port Node.js runs on, which we will assume is 5000. At the following rules to end of the VirtualHost section corresponding to your domain in httpd.conf (be sure to do this as a user with permissions to edit httpd.conf):

ProxyRequests Off
ProxyPreserveHost On
ProxyVia Full
<Proxy *>
    Require all granted
</Proxy>
ProxyPass / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5000/

Restart the Apache HTTP server for the proxy rules to take effect.

Install and Configure pm2 #

Installing the pm2 process manager will help us to run the Node.js server in the background, which will eliminate the need to manually start it with “npm start” and having to monitor that the command stays running. If you’re not familiar with the term “daemon”, it simply means a process that runs in the background, so we’re effectively “daemonizing” the Node.js process.

npm install pm2@latest -g

In the “nodejs” folder, create a file named ecosystem.config.js and add this to it:

module.exports = {
  apps : [
      {
        name: "node_server",
        script: "npm start",
        watch: true,
        env: {
            "PORT": 5000,
            "NODE_ENV":"production"
        }
      }
  ]
}

You can change “node_server” to anything you want to give a name for pm2 to use to identify your Node.js server. If your server is set up to start with a different command, you should replace “npm start” with that.

At this point, you would run “pm2 start ecosystem.config.js” to start the Node.js daemon process, but if you have “type”: “module” in package.json, you will run into the issue reported at https://github.com/Unitech/pm2/issues/4540. As of 2/25/2021, this issue has not been resolved. If you do not have “type”: “module”, then you might be able to sidestep this issue.

Fortunately, there is an easy workaround involving just a few steps:

1] Remove “type”: “module” from package.json
2] Run: pm2 start ecosystem.config.js (running “pm2 log” after this shows errors about your server code using the “import” keyword)
3] Add “type”: “module” back in (running “pm2 log” after this shows the server has recovered)
4] Run “pm2 ls” to see that the Node.js server is running.

That’s it! Hopefully pm2 will resolve this issue and we wouldn’t have to resort to this workaround.

Run pm2 as a Service #

pm2 needs to know about the running daemon process so that it can be revived when the system reboots and runs the service we are about to create. To save the process, run:

pm2 save

Now run this command to show instructions for creating a service:

pm2 startup

As the instructions says, you will need to run the printed command with sudo privilege or as the root user. Go ahead and do that. Your operating system should now have a service named something like pm2-joe.service.

Now when your system reboots, the Node.js server will automatically be run by pm2 (even with “type”: “module” workaround above used). Enjoy!

Leave a comment

Back to Top