The ultimate guide to deploying your node app on Linux

January 25, 2020

This time we do the work for you.

an image

By Mike on 12th Feb 2018

This is the latest version of the most popular guide to deploying node on Linux on the internet, which I've kept updated for the last 5 years with input from the community.

It sets up:

  • A Long Term Support (LTS) version of node on an LTS version of Linux.
  • Required tools to build binary packages.
  • yarn, for fast, consistent deploys.
  • Easy security updates for the above via apt-get.
  • The ability to deploy automatically using GitHub / GitLab deploy keys, without needing a password.
  • Automated restarts, proper logging, and other good things by a proper systemd service for your node app.
  • The ability for you to log in automatically using your SSH key, without needing a password, as your own account, and with full sudo access.

2018 changes

Changes this year are:

  • We're switched to yarn. As a repository we love npm, as a client the npm 5 rollout hasn't been great - there's a number of bugs that have affected us and still haven't been addressed. Additionally, yarn workspaces work great with the monorepo development pattern used by projects like Babel, React, Angular, Ember, Meteor, Jest, and EV HTTPS verification legends CertSimple. Yes that's us.

  • We've moved our load balancer guides elsewhere. We've got links to different strategies for load balancing at the end of the article.

  • We've also focused on Ubuntu here and removed the RHEL / CentOS instructions - focusing on a single, popular distro avoids cluttering the guide up with minor differences.

Oh yeah and one more thing:

This time we're doing the work for you.

While the guide still covers the 'why' and 'how' of deploying node - we also do all the above for you using our Cloud Init. Just:

...and you'll have a custom cloud-init.yaml file for AWS, Azure, Digital Ocean and most other cloud providers.

Specify the Cloud Init as 'User Data' when you're building a box (either manually or via the API):

an image

The build takes around 30 seconds on a Digital Ocean droplet. Once it's complete, skip straight to deploying.

Contents

Install Linux

Add an admin user

Set up user accounts and SSH keys

Set timezone to UTC

Install node, yarn and build tools

Setup git and deploy keys

Add a shrinkwrap file, for consistent deploys

Create services

Deploying

Setup HTTPS and load balancing

Conclusion

Install an OS + the bits necessary for node

  • Install an LTS release: right now, that's Ubuntu 16.04 or Debian Jessie
  • You want the 64 bit version so you can use all your memory.
  • Install all updates:
# Debian / Ubuntu
apt-get update
apt-get -y dist-upgrade

Add an admin user

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 user. 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:

# Debian / Ubuntu
useradd -m -G admin -s /bin/bash mike

Enable passwordless sudo access to people in the 'admin' 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:

# Debian / Ubuntu
%admin 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 something like:

User max may run the following commands on somebox:
  (ALL) NOPASSWD: ALL

? Good. You now have a working admin account. You can run

sudo someCommand

To run a single command or:

sudo -i

To run an interactive shell.

Set up passwordless log in using SSH public keys

If you're using GitHub: you can find a copy of your user's public keys at https://github.com/(their GitHub user name).keys

Otherwise, 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 computer, and if the file doesn't exist, run ssh-keygen to make a new one.

Make sure people only give you the .pub keys, otherwise they'll have to regenerate everything.

Got the users keys? Copy them, one key per line, into ~/.ssh/authorized_keys on the box (note the American English spelling). Doing this allows whoever has the corresponding private key to log in as that user.

Check you can log in using ssh without needing a pasword.

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 let's 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 an LTS Node

Install node 8, the current LTS release, yarn, and development tools. The latter are needed to compile binary modules — e.g., yarn add node-sass would need the development tools to build libsass.

The lovely people at NodeSource make official packages of node for most distros and yarn produce their own OS packages too.

# Add nodejs repo
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -

# Add yarn repo
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list

# install node and yarn and development tools
sudo apt-get update
sudo apt-get install -y nodejs yarn build-essential

Add a yarn lock file, for consistent deploys

yarn's inbuilt yark.lock files ensures that your dependencies - specifically your dependencies dependencies - remain the same. So the package versions you use on your developer machine are the same as what ends up on the server. yarn.lock is automatically updated when you add a dependency, so just commit the yarn.lock file.

git add yarn.lock

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 myapp@mycompany.com
  • For GitHub : on your profile page, click Repositories, the name of your repository, Settings, Deploy Keys, then Add deploy key.

an image

HTTPS and Load Balancing

You should really be using HTTPS these days.

Since you'll probably have at least two app servers, you're best to terminate your HTTPS on a load balancer: this means a single place for your HTTPS to be handled before it's passed on via straight HTTP to one of your running node servers.

  • In AWS, set up an AWS Application Load Balancer (ALB, also called ELB version 2). These are an AWS-specific load balancing application, and that's where you'd install your HTTPS certificate and key.

  • In Digital Ocean, set up Digital Ocean's load balancer. Install your HTTPS certificate and key, point it at your app servers and go. Note DO Load Balancers only suport the older, slower HTTP1.1, so you might prefer HAProxy.

  • Otherwise, use HAProxy. It's a high availability proxy, that will sit in front of your appservers, handle HTTPS, and direct customers to the current active server. Our guide sets up a load balancer with HTTP/2 and dynamic reconfig.

  • You might also be interested in nginx, but the Open Source version lacks features compared to HAProxy.

Once you've set your load balancer, check your work at SSL Labs - you should get at least an A.

Create 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, understand how to change user accounts, make sure we clear environment variables, set up automatic restarts, log to weird syslog locations like 'local3', and other necessary, yet overly complex work.

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=Your app
After=network.target
[Service]
ExecStart=/var/www/myapp/app.js
Restart=always
User=nobody
# Use 'nogroup' group for Ubuntu/Debian
# use 'nobody' group for Fedora
Group=nogroup
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
WorkingDirectory=/var/www/myapp
[Install]
WantedBy=multi-user.target

Here are the more interesting lines:

After means we'll start this service after the network service has started.

ExecStart is the app to run. It should have an interpreter on the first line of the file — eg, you'll want to add #!/usr/bin/env node to app.js if it's not already there. You'll also want the file to be executable: chmod +x app.js

Environment lines are the environment variables — setting NODE_ENV to production is a node standard to tell apps like Express and gulp to run on your production settings.

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

sudo systemctl daemon-reload

Make the service start on boot:

sudo systemctl enable application

Deploying

After this you can SSH into the box and:

  • Add ~/.ssh/id_dsa.pub to your project on GitHub / GitLab as a deploy key. You can now cd to /var/www and run git clone and your project will pull down.
  • Run yarn install to pull down dependencies and yarn build to run build tasks
  • Start your app with sudo systemctl start (app name).

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:

sudo 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 using journalctl, fix anything it tells you about, then restart the app with:

sudo systemctl restart myapp

Your app should soon be up and running.

Deploying 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
yarn install
yarn build
sudo systemctl restart myapp

One final note

So recapping everything everything we've done:

  • Long Term Support (LTS) version of node, installed properly on an LTS version of Linux, and yarn for repeatable installs.
  • A proper service for your node app, with automated restarts, proper logging, and other good things.
  • Your app running as the user you specified in the .service file.
  • The ability for you to log in automatically using your SSH key, as your own account, and with sudo access.
  • The ability to deploy via git using deploy keys
  • Keys for everything and no passwords anywhere!
  • The ability to instantly create new instances as needed via Cloud Init.

I hope that was useful — if you have additions or corrections feel free to let us know in the corresponding Hackers News post.