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.