-
Notifications
You must be signed in to change notification settings - Fork 28
Packaging a Node.js project as an RPM for CentOS 7
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.
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
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
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
# > [email protected] 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.
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
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.