How I deploy node apps on Linux, 2015 edition

A short, practical guide.


There’s a lot that has changed in Linux lately, and much of the documentation focuses on the technology is for infrastructure specialists rather than node developers. If you want to run your own servers — and maybe build your own PAAS — here’s how to deploy your node app on a modern Linux setup.

You shouldn’t need to do this often: once your system is installed, so you don’t have to repeat the work again, save it:

  • Cloud providers let make an images of installed systems in their control panel. For example, in AWS, make an Amazon Machine Image (AMI) of your EC2 instance.
  • For docker, simply save your command history to a Dockerfile.
  • If you’re using a config management tool (I like Ansible), create a playbook from the commands and config files below.

Our aims are simple:

  • Our node app, running as a service, as an unprivileged user
  • Secure access to the system by developers and admins

If you just want to make a .service file for your node app, skip ahead to ‘Install and Start Services’ below.

Install an OS

  • Install RHEL/CentOS 7, Debian 7 stable (Wheezy), or Ubuntu 14.04 LTS. It doesn’t really matter that much these days. Personally I like Red Hat, mainly because Red Hat has some very comprehensive official docs (these apply to CentOS too). However I used to work for Red Hat a long time ago so I’m probably biased.
    Whatever distribution you pick, you want the 64 bit version so you can use the RAM you purchased.
  • Install all updates
yum -y update
  • Install git
yum -y install git
  • Install development tools. On RHEL, that’s:
yum group install “Development Tools”

Allow your team to log in with their public keys


Your cloud provider might already set up a user account and public key access — for example, AWS create ‘ec2-user’ with sudo access and your public keys already authorised to log in to this account. If that’s the case, skip ahead.

However some cloud providers (like Digital Ocean) just give you a root prompt. Logging in as root all the time is considered somewhat insecure — for one thing, everyone already knows the user name, so let’s make a regular user account:

useradd myaccount

Wheel is an old Unix term that apparently comes from the expression ‘big wheel’ meaning a powerful person. For all intents and purposes, it’s your ‘admin’ group. Add the user you just made to the ‘wheel’ secondary group:

usermod -G wheel myaccount

Ask each member of your team for a copy of their public key. If they’re not sure, check the ~/.ssh/id_rsa.pub file on their Mac or Linux box, and if the file doesn’t exist, run ssh-keygen to make a new one.

Make sure people only give you the .pub keys, and not the non-pub keys, otherwise they’ll have to regenerate everything.

Got all those keys? Copy them, one key per line, into ~/.ssh/authorized_keys on the box (note the American English spelling). By putting the public keys into a user’s authorized_keys this allows anyone with the corresponding private key to log in as that user.

Enable passwordless sudo access to people in the ‘wheel’ group — this means you won’t have to retype your password to run sudo. Editing the sudoers file is a bit special: if you mess it up, you could lock yourself out. So run:

visudo

…which just opens your editor on the sudo file, and checks the syntax before you save. Uncomment the line that looks like:

%wheel ALL = (ALL) NOPASSWD: ALL

Now try logging in as a regular user from the outside world. You should be able to log in using your public key (ie, without needing a password) and be able to run

sudo -l

to list your access. Does it say a lot of ALLs? Good. You now have a working wheel (admin) account. You can run

sudo someCommand

To run a single command or:

sudo -i

To run an interactive shell.

Let’s disable root logins over SSH then.

Edit /etc/ssh/sshd_config, find the ‘PermitRootLogin’ line and change it to ‘no’. Then run:

 systemctl reload sshd

To make the changes apply.

Set timezone to UTC

There’s a whole bunch of reasons why you might want to set the time on your server to UTC. First lets update the timezone:

rm /etc/localtime
ln -s /usr/share/zoneinfo/UTC /etc/localtime

To check your work, look at the local time right now:

date

The answer should be the same as the UTC date:

date -u

Install Node


Using Packages

If your distribution has an up to date Node package, install that. For example, there are Red Hat Enterprise Linux and CentOS packages at: https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager#enterprise-linux-and-fedora. If that’s you, install the packages then skip ahead.

If you can’t find a Node package for your OS

If not: visit http://nodejs.org/download/, download node, and extract it into /usr/local

tar -xf node-someversion.tgz

Make some symlinks so node is in your PATH:

ln -s /usr/local/node/bin/node /usr/local/bin/node
ln -s /usr/local/node/bin/npm /usr/local/bin/npm

Install MongoDB (if you need it)

If your cloud provider has a hosted MongoDB, you should probably use that instead of installing your own.

But if you do need Mongo, first check if your OS already has a packaged, up to date version of MongoDB — if so, install and use that. If not, download and install MongoDB from the official site. Make a mongod user:

useradd mongod -s /sbin/nologin

Next add the service. The next version of MongoDB already has a mongod.service file included with it, but if not, copy this into /etc/systemd/system

[Unit]
Description=High-performance, schema-free document-oriented database
After=network.target
[Service]
User=mongod
Group=mongod
Environment=”OPTIONS=—quiet -f /etc/mongod.conf”
ExecStart=/usr/local/bin/mongod $OPTIONS run
PIDFile=/var/run/mongodb/mongod.pid
[Install]
WantedBy=multi-user.target

Also create /etc/mongod.conf, which the service uses:

dbpath = /var/mongodb
pidfilepath = /var/run/mongodb/mongod.pid

Load the recently added service file:

systemctl daemon-reload

Start the mongod service:

systemctl start mongod.service

Setup git


We’re going to update our apps using git. If you use GitHub or GitLab, rather than using a particular person’s SSH key for git, you can create a special ‘deploy key’ which only has read access to a single project. You’d normally make the key on the machine:

ssh-keygen -t rsa -C “myuser@mycompany.com”

Add the deploy key to GitHub or GitLab.

Allow Inbound Access to Your App’s Ports


Pull down your app — make, then change into /var/www and ‘git clone’ your app.

On Unix, only the root user can bind to ports below 1024. We don’t want to run our app as root though, as that’s not secure.

So we run web apps on a high port so we don’t need to be root, but redirect port 80 traffic to there:

Red Hat / CentOS


For Red Hat, use the inbuilt firewalld package. Note on AWS you may have to install the firewalld RPM first:

firewall-cmd --zone=public --add-forward-port=port=80:proto=tcp:toport=8000 --permanent

If the app has additional ports that need to be accessed from the outside world (eg, an API, which runs on port 3000) then open them up.

firewall-cmd --zone=public --add-port=3000/tcp --permanent

Apply the updated firewall rules:

firewall-cmd --reload

And see the applied rules with:

firewall-cmd --zone=public --list-all

Debian / Ubuntu


In Debian / Ubuntu, iptables rule are applied directly — as soon as you run the following commands the port will be open:

iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A PREROUTING -t nat -p tcp --dport 80 -j REDIRECT --to-port 8000

To save the rules out to a config file later:

iptables-save > /etc/iptables/rules.v4

The config file will be used by the iptables-persistent service when the machine boots.

Install and Start Services


Services on all popular Linux distributions now use systemd. This means we don’t have to write shell scripts, explore the wonders of daemonization, changing user accounts, clearing environment variables, set up automatic restarts, log to weird syslog locations like ‘local3' , and a bunch of other stuff.

Instead, we just make a .service file for the app and let the OS take care of these things. Here’s an example one, called myapp.service:

[Unit]
Description=My app
After=network.target
[Service]
ExecStart=/var/www/myapp/app.js
Restart=always
User=nobody
Group=nobody
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
WorkingDirectory=/var/www/myapp
[Install]
WantedBy=multi-user.target

Description is a friendly description.
After means we’ll start this service after the network service has started.

ExecStart is the app to run. You could run

Copy your service file into the /etc/systemd/system directory. Then make systemd load up the new service file:

 systemctl daemon-reload

Start the service:

 systemctl start myapp

All your node console output is logged to the journal with the same name as your .service file. To watch logs for ‘myapp’ in realtime:

 journalctl --follow -u myapp

Unless you’re perfect the first time you run your app you might have the odd problem — you forgot to run gulp, file permissions are incorrect, etc. Read the log output, fix anything it tells you about, then restart the app with:

systemctl restart myapp

Your app should soon be up and running.

Updating

Updating should be a matter of cleaning any generated files, pulling the latest code, installing whatever new packages your node-shrinkwrap file specifies, and restarting the service:

git clean -f -d
git pull origin/master
npm install
gulp build
systemctl restart myapp

Got it working? As we said earlier: you can, and should, of course, automate this. Throw all the logic into ansible, puppet, chef, or bake an image (for example, and EC2 AMI) of your instance.

Finally

I hope that was useful —while this is the best practice I know of right now, if you have additions or corrections feel free to comment here or the corresponding Hackers News post and I’ll do my best to keep this post updated.