Skip to content

Packaging a Node.js project as an RPM for CentOS 7

Niklas Rahmel edited this page Apr 13, 2017 · 2 revisions

There are lots of ways to deploy a Node.js application in production. You could upload your application to a server and start it just as you would in development. You could keep it running with a process manager like forever or pm2. You could use a container technology like Docker. Alternatively, you could use the established packaging standard for your target platform. For CentOS and Red Hat Enterprise Linux, that means deploying your application as an RPM.

One of the advantages of using an existing packaging standard like RPM (or .deb for Debian/Ubuntu platforms) is that you can deploy applications written in different languages in exactly the same way. This might not seem important if you're only building Node.js applications, but if your organisation works with different languages it can bring some much-needed sanity to the deployment process.

An RPM package can also depend on other packages, which provides a natural way for you to specify your OS-level dependencies when they arise (ImageMagick, for example.)

In order to package your application as an RPM you need to create a spec file to describe your package. In this guide we'll use the speculate tool to generate the spec file automatically from an existing package.json file. We'll then use the rpmbuild tool to create the RPM package. We'll use Vagrant to spin up a CentOS 7 virtual machine where we can build and test the RPM.

We'll also be using systemd to start, stop and automatically restart our application if it stops unexpectedly. Just as choosing RPM as a packaging standard can simplify how you deploy applications written in different languages, using a native process management tool over a language specific solution can also simplify how you manage your applications in production.

Create the Node project

We're going to create a simple Node project to use as an example. Create a new directory and run the npm init command, accepting all of the default options:

mkdir my-cool-api
cd my-cool-api
npm init -y

Install Express as a dependency:

npm install --save express

Create the following simple hello world application in a server.js file:

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello world');
});

app.listen(8080, () => {
  console.info('Server started at http://127.0.0.1:8080');
});

Check that the server can be started by running node server.js (you'll need Node 4.x or greater to run the example above):

node server.js
# Server started at http://127.0.0.1:8080

Use your browser or curl to ensure that the server responds as expected:

curl http://127.0.0.1:8080
# Hello world

Set up the build environment

Now that we've got a simple Node.js application ready to deploy, we can set up a clean environment for building and testing the RPM package. We're going to use Vagrant and VirtualBox, so make sure they're installed.

Start by creating a new CentOS 7 virtual machine:

vagrant init centos/7
vagrant up --provider virtualbox
vagrant ssh
# [vagrant@localhost ~]$

For the remainder of this guide we'll be running all of the commands inside the new virtual machine.

Vagrant copies our example application to ~/sync on the virtual machine:

ls ~/sync
# node_modules  package.json  server.js  Vagrantfile

We'll need Node.js installed to build and test our application. Nodesource helpfully provides binary distributions of Node.js for Linux platforms. We'll use their Node 4 repository to install the nodejs package:

curl -sL https://rpm.nodesource.com/setup_4.x | sudo bash -
sudo yum install -y nodejs

We can check that Node has been insalled by running node --version:

node --version
# v4.4.0

We'll also install the dependencies we need for building the RPM:

sudo yum install -y rpm-build redhat-rpm-config

The rpmbuild command expects our application to live in the ~/rpmbuild directory. Instead of moving our source files into the directory, we can just link them:

ln -s ~/sync ~/rpmbuild

Generate the spec file

Now that our build environment is set up, we can create a spec file for our project. We could do this manually, but the speculate tool can do this automatically using information that's already in our package.json file.

Let's start by installing speculate as a local npm dependency:

cd ~/rpmbuild
npm install --save-dev speculate

We can then create an npm script inside our package.json file to run the speculate command. This means we can avoid installing the speculate command globally.

{
  "scripts": {
    "start": "node server.js",
    "spec": "speculate"
  }
}

Let's run the script to generate our spec file:

npm run spec
# > my-cool-api@1.0.0 spec ~/rpmbuild
# > speculate
#
# Created ./SPECS/my-cool-api.spec
# Created ./SOURCES/my-cool-api.tar.gz
# Created ./my-cool-api.service

You'll now see that the speculate tool has created a spec file for us (in SPECS/my-cool-api.spec). It also creates a systemd service definition file (my-cool-api.service) and a .tar.gz archive that contains our application:

rpmbuild
├── my-cool-api.service
├── node_modules
│   ├── express
│   └── speculate
├── package.json
├── server.js
├── SOURCES
│   └── my-cool-api.tar.gz
├── SPECS
│   └── my-cool-api.spec
└── Vagrantfile

The SPECS and SOURCES directories will be used by the rpmbuild command when we build our RPM.

Build the RPM

We can now run the rpmbuild commmand to build an RPM from our generated spec file:

rpmbuild -bb ~/rpmbuild/SPECS/my-cool-api.spec

The -bb flag tells rpmbuild to create a full binary RPM. You can use other flags to perform only part of the full RPM packaging process.

Once the process is complete, we should have an RPM package containing our application in the ~/rpmbuild/RPMS/x86_64 directory:

ls ~/rpmbuild/RPMS/x86_64
# my-cool-api-1.0.0-1.x86_64.rpm  my-cool-api-debuginfo-1.0.0-1.x86_64.rpm

You can see that the command actually created two .rpm files. The file that we're interested in is my-cool-api-1.0.0-1.x86_64.rpm. We won't use the debuginfo package - it just contains information about how the application was packaged for our particular architecture.

We can list all of the files contained in the RPM using the rpm tool:

rpm -qpl ~/rpmbuild/RPMS/x86_64/my-cool-api-1.0.0-1.x86_64.rpm
# /usr/lib/my-cool-api
# ...

The spec file generated by speculate puts our application code in /usr/lib/my-cool-api. You'll see all of our application files are listed, as well as the node_modules directory and its contents.

It's a good idea to add the files generated by rpmbuild and speculate to your .gitignore file:

SPECS
SOURCES
BUILD
BUILDROOT
SRPMS
RPMS
*.service

Test the RPM

Now that our RPM is built, we can install it locally:

sudo yum install -y ~/rpmbuild/RPMS/x86_64/my-cool-api-1.0.0-1.x86_64.rpm

Once the RPM is installed, we can start the server using the systemctl command:

sudo systemctl start my-cool-api

This works because speculate created a systemd service definition file for us. We can also use systemctl to check on the status of the service by running:

sudo systemctl status my-cool-api

You should see that the service is running, and we can access the server using curl:

curl http://127.0.0.1:8080
# Hello world

We've seen how to package our simple Node.js application as an RPM for deployment on a CentOS 7 instance. Using the speculate tool, we were able to automatically generate a spec file that could be used with rpmbuild to create the RPM.

Whilst rpmbuild is a useful tool for building RPMS quickly, it's important to run it in a clean build environment to ensure predictable builds. A more sophisticated alternative to rpmbuild is mock - a build tool lets you create your RPMS in a fully sandboxed environment.