While setting up a Node.js environment on an individual developer’s machine can be done in a casual manner and oftentimes can be tailored to the developer’s own taste, deploying Node.js applications on shared or production servers requires a little more planning in advance.
To install Node.js on a server, a straight forward approach is to just follow some quick-start instructions from an official source. For instance, assuming latest v.4.x of Node.js is the target version and CentOS Linux is the OS on the target server, the installation can be as simple as follows:
# Install EPEL (Extra Packages for Enterprise Linux) sudo yum install epel-release # Run Node.js pre-installation setup curl -sL https://rpm.nodesource.com/setup_4.x | bash - # Install Node.js sudo yum install -y nodejs For Ubuntu: # Install Node.js on Ubuntu curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash - sudo apt-get install -y nodejs
Software version: Latest versus Same
However, the above installation option leaves the version of the installed Node.js out of your own control. Although the major release would stick to v.4, the latest update to Node available at the time of the command execution will be installed.
There are debates about always-getting-the-latest versus keeping-the-same-version when it comes to software installation. My take is that on individual developer’s machine, you’re at liberty to go for ‘latest’ or ‘same’ to suit your own need (for exploring experimental features versus getting ready for production support). But on servers for staging, QA, or production, I would stick to ‘same’.
Some advocates of ‘latest’ even for production servers argue that not doing so could compromise security on the servers. It’s a valid concern but stability is also a critical factor. My recommendation is to keep version on critical servers consistent while making version update for security a separate and independently duty, preferably handled by a separate operations staff.
Onto keeping a fixed Node.js version
As of this writing, the latest LTS (long-term-support) release of Node.js is v.4.4.7. The next LTS (v.6.x) is scheduled to be out in the next quarter of the year. There are a couple of options. Again, let’s assume we’re on CentOS, and that it’s CentOS 7 64-bit. There are a couple of options.
Option 1: Build from source
# Install Node.js v4.4.7 - Build from source mkdir ~/nodejs cd ~/nodejs curl http://nodejs.org/dist/v4.4.7/node-v4.4.7.tar.gz | tar xz --strip-components=1 ./configure --prefix=/usr/local sudo make install
As a side note, if you’re on CentOS 6 or older, you’ll need to update gcc and Python.
Option 2: Use pre-built binary
# Install Node.js v4.4.7 - Linux binary (64bit) mkdir ~/nodejs cd ~/nodejs curl http://nodejs.org/dist/v4.4.7/node-v4.4.7-linux-x64.tar.gz | tar xz --strip-components=1 # Install Node under /opt mkdir ~/nodejs/etc echo 'prefix=/usr/local' > nodejs/etc/npmrc sudo mv nodejs /opt/ sudo chown -R root:root /opt/nodejs # Create soft links in standard search path sudo ln -s /opt/nodejs/bin/node /usr/local/bin/node sudo ln -s /opt/nodejs/bin/npm /usr/local/bin/npm
Note that both the above two options install a system-wide Node.js (which comes with the default package manager NPM) accessible to all legitimate users on the server host.
Node process manager
Next, install a process manager to manage processes of the Node app, providing features such as auto-restart. Two of the most prominent ones are forever and pm2. Let’s go with the slightly more robust one, pm2. Check for the latest version from the pm2 website and specify it in the npm install command:
# Install global pm2 v1.1.3 sudo npm install -g pm2@1.1.3 # Verify installed pm2 cd /usr/local/lib npm list | grep pm2
Deploying self-contained Node.js
Depending on specific deployment requirements, one might prefer having Node confined to a local file structure that belongs to a designated user on the server host. Contrary to having a system-wide Node.js, this approach would equip each of your Node projects with its own Node.js binary and modules.
Docker, as briefly touched on in a previous blog, would be a good tool in such use case, but one can also handle it without introducing an OS-level virtualization layer. Here’s how Node.js can be installed underneath a local Node.js project directory:
# Project directory of your Node.js app PROJDIR="/path/to/MyNodeApp" # Install local Node.js v4.4.7 Linux binary (64bit) mkdir $PROJDIR/nodejs cd $PROJDIR/nodejs curl http://nodejs.org/dist/v4.4.7/node-v4.4.7-linux-x64.tar.gz | tar xz --strip-components=1 # Install local pm2 v1.1.3 # pm2 will be installed under $PROJDIR/nodejs/lib/node_modules/pm2/bin/ cd $PROJDIR/nodejs/lib sudo $PROJDIR/nodejs/bin/npm install pm2@1.1.3 $PROJDIR/nodejs/bin/npm list | grep pm2
Next, create simple scripts to start/stop the local Node.js app (assuming main Node app is app.js):
Script: $PROJDIR/bin/njsenv.sh (sourced by start/stop scripts)
# $PROJDIR/bin/njsenv.sh #!/bin/bash ENVSCRIPT="$0" # Get absolute filepath of this setenv script ENVBINPATH="$( cd "$( dirname "$ENVSCRIPT" )" && pwd )" # Get absolute filepath of the Nodejs project PROJPATH="$( cd "$ENVBINPATH" && cd ".." && pwd )" # Get absolute filepath of the Nodejs bin NJSBINPATH="$( cd "$PROJPATH" && cd nodejs/bin && pwd )" # Get absolute filepath of the process manager PMGRPATH=${PROJPATH}/nodejs/lib/node_modules/pm2/bin # Function for prepending a path segment that is not yet in PATH pathprepend() { for ARG in "$@" do if [ -d "$ARG" ] && [[ ":$PATH:" != *":$ARG:"* ]]; then PATH="$ARG${PATH:+":$PATH"}" fi done } pathprepend "$PMGRPATH" "$NJSBINPATH" echo "PATH: $PATH"
Script: $PROJDIR/bin/start.sh
#!/bin/bash SCRIPT="$0" # Get absolute filepath of this script BINPATH="$( cd "$( dirname "$SCRIPT" )" && pwd )" if [ ! -f "${BINPATH}/njsenv.sh" ] then echo "${BINPATH}/njsenv.sh cannot be found! Aborting ..." exit 0 fi # Set env for PATH and project/app: # PATH = Linux path # PROJPATH = Nodejs project # NJSBINPATH = Nodejs bin # PMGRPATH = pm2 path source ${BINPATH}/njsenv.sh NODEAPP="main.js" PMGR=${PMGRPATH}/pm2 echo "Starting $NODEAPP at $PROJPATH ..." CMD="cd $PROJPATH && $PMGR start $NODEAPP" # Start Nodejs main app eval $CMD echo "Command executed: $CMD"
Script: $PROJDIR/bin/stop.sh
#!/bin/bash SCRIPT="$0" # Get absolute filepath of this script BINPATH="$( cd "$( dirname "$SCRIPT" )" && pwd )" if [ ! -f "${BINPATH}/njsenv.sh" ] then echo "${BINPATH}/njsenv.sh cannot be found! Aborting ..." exit 0 fi # Set env for PATH and project/app: # PATH = Linux path # PROJPATH = Nodejs project # NJSBINPATH = Nodejs bin # PMGRPATH = pm2 path source ${BINPATH}/njsenv.sh # Set PATH env source ${BINPATH}/njsenv.sh PMGR=${PMGRPATH}/pm2 echo "Stopping all Node.js processes ..." CMD="cd $PROJPATH && $PMGR stop all" # Stop all Nodejs processes eval $CMD echo "Command executed: $CMD"
It would make sense to organize such scripts in, say, a top-level bin/ subdirectory. Along with the typical file structure of your Node app such as controllers, routes, configurations, etc, your Node.js project directory might now look like the following:
$PROJDIR/ app.js bin/ njsenv.sh start.sh stop.sh config/ controllers/ log/ models/ nodejs/ bin/ lib/ node_modules/ node_modules/ package.json public/ routes/ views/
Packaging/Bundling your Node.js app
Now that the key Node.js software modules are in place all within a local $PROJDIR subdirectory, next in line is to shift the focus to your own Node app and create some simple scripts for bundling the app.
This blog post is aimed to cover relatively simple deployment cases in which there isn’t need for environment-specific code build. Should such need arise, chances are that you might already be using a build automation tool such as gulp, which was heavily used by a Node app in a recent startup I cofounded. In addition, if the deployment requirements are complex enough, configuration management/automation tools like Puppet, SaltStack or Chef might also be used.
For simple Node.js deployment that the app modules can be pre-built prior to deployment, one can simply come up with simple scripts to pre-package the app in a tar ball, which then gets expanded in the target server environments.
To better manage files for the packaging/bundling task, it’s a good practice to maintain a list of files/directories to be included in a text file, say, include.files. For instance, if there is no need for environment-specific code build, package.json doesn’t need to be included when packaging in the QA/production environment. While at it, keep also a file, exclude.files that list all the files/directories to be excluded. For example:
# File include.files: app.js config controllers models node_modules nodejs public routes views # File exclude.files: .DS_Store .git
Below is a simple shell script which does the packaging/bundling of a localized Node.js project:
#!/bin/bash # Project directory of your Node.js app PROJDIR="/path/to/MyNodeApp" # Extract package name and version NAME=`grep -o '"name"[ \t]*:[ \t]*"[^"]*"' $PROJDIR/package.json | sed -n 's/.*:[ \t]*"\([^"]*\)"/\1/p'` VERSION=`grep -o '"version"[ \t]*:[ \t]*"[^"]*"' $PROJDIR/package.json | sed -n 's/.*:[ \t]*"\([^"]*\)"/\1/p'` if [ "$NAME" = "" ] || [ "$VERSION" = "" ]; then echo "ERROR: Package name or version not found! Exiting ..." exit 1 fi # Copy files/directories based on 'files.include' to the bundle subdirectory cd $PROJDIR # Create/Recreate bundle subdirectory rm -rf bundle mkdir bundle mkdir bundle/$NAME for file in `cat include.files`; do cp -rp "$file" bundle/$NAME ; done # Tar-gz content excluding files/directories based on 'exclude.files' cd bundle tar --exclude-from=../exclude.files -czf $NAME-$VERSION.tar.gz $NAME if [ $? -eq 0 ]; then echo "Bundle created under $PROJDIR/bundle: $NAME-$VERSION.tar.gz else echo "ERROR: Bundling failed!" fi rm -rf $NAME
Run bundling scripts from within package.json
An alternative to doing the packaging/bundling with external scripts is to make use of npm’s features. The popular Node package manager comes with file exclusion rules based on files listed in .npmignore and .gitignore. It also comes with scripting capability that to handle much of what’s just described. For example, one could define custom file inclusion variable within package.json and executable scripts to do the packaging/bundling using the variables in the form of $npm_package_{var} like the following:
"name": "mynodeapp", "version": "1.0.0", "main": "app.js", "description": "My Node.js App", "author": "Leo C.", "license": "ISC", "dependencies": { "config": "~1.21.0", "connect-redis": "~3.1.0", "express": "~4.14.0", "gulp": "~3.9.1", "gulp-mocha": "~2.2.0", "helmet": "~2.1.1", "lodash": "~4.13.1", "mocha": "~2.5.3", "passport": "~0.3.2", "passport-local": "~1.0.0", "pg": "^6.0.3", "pg-native": "^1.10.0", "q": "~1.4.1", "redis": "^2.6.2", "requirejs": "~2.2.0", "swig": "~1.4.2", "winston": "~2.2.0", }, "bundleinclude": "app.js config/ controllers/ models/ node_modules/ nodejs/ public/ routes/ views/", "scripts": { "bundle": "rm -rf bundle && mkdir bundle && mkdir bundle/$npm_package_name && cp -rp $npm_package_bundleinclude bundle/$npm_package_name && cd bundle && tar --exclude-from=../.npmignore -czf $npm_package_name-$npm_package_version.tgz $npm_package_name && rm -rf $npm_package_name" } }
Here comes another side note: In the dependencies section, a version with prefix ~ qualifies any version with patch-level update (e.g. ~1.2.3 allows any 1.2.x update), whereas prefix ^ qualifies minor-level update (e.g. ^1.2.3 allows any 1.x.y update).
To deploy the Node app on a server host, simply scp the bundled tar ball to the designated user on the host (e.g. scp $NAME-$VERSION.tgz njsapp@:package/) use a simple script similar to the following to extract the bundled tar ball on the host and start/stop the Node app:
#!/bin/bash if [ $# -ne 2 ] then echo "Usage: $0 " echo " e.g. $0 mynodeapp ~/package/mynodeapp-1.0.0.tar.gz" exit 0 fi APPNAME="$1" PACKAGE="$2" # Deployment location of your Node.js app DEPLOYDIR="/path/to/DeployDirectory" cd $DEPLOYDIR tar -xzf $PACKAGE if [ $? -ne 0 ]; then echo "Package $PACKAGE extracted under $DEPLOYDIR" else echo "ERROR: Failed to extract $PACKAGE! Exiting ..." exit 1 fi # Start Node app $DEPLOYDIR/$APPNAME/bin/start.sh
Deployment requirements can be very different for individual engineering operations. All that has been suggested should be taken as simplified use cases. The main objective is to come up with a self-contained Node.js application so that the developers can autonomously package their code with version-consistent Node binary and dependencies. A big advantage of such approach is separation of concern, so that the OPS team does not need to worry about Node installation and versioning.