Node.js Deployment on Self-Managed Server

This is the cheapest self-hosting solution for small projects. You can start a production-capable cloud compute instance for 5/mo, billed hourly – ie 5/mo is roughly 2 tenths of a penny per hour, so if you only run the compute instance for two hours you’ll owe almost half a penny.

Create an account on Linode

(Linode is now Akamai)

Referral link below. Free $100 credit, I get $25 if you end up spending $25: 

https://www.linode.com/lp/refer/?r=43323ed57bf1cdb87800402242b6d3ea92843bfd

Create an Ubuntu linode and log in via ssh

Create a new Linode, select Ubuntu.

You can login using the root user, but best practice is creating a new user.

Install Node/NPM with NVM

Update our package list and upgrade our installed packages. Node Version Manager (NVM) is officially recommended by NodeJS: https://nodejs.org/en/download

sudo apt update
sudo apt upgrade

sudo apt install -y ca-certificates curl gnupg

mkdir -p /etc/apt/keyrings

# Download and install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

# Make sure NVM environment variables are set up
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

# in lieu of restarting the shell
\. "$HOME/.nvm/nvm.sh"

# Download and install Node.js:
sudo nvm install node

# Verify the Node.js version:
node -v

# Verify npm version:
npm -v

Clone your project from Github

There are a few ways to get your files on to the server, I might suggest using Git with Github. Create an SSH key on your server to authenticate with Github, so you can pull repositories from your Github account and pull them into your server.

git clone yourproject.git

Install dependencies and test app

cd yourproject
npm install
npm start

Stop app using ctrl+C

Setup PM2 process manager to keep your app running

In the root of your project directory, add a file called ecosystem.config.js and add the following.

Note: it’s better to add this ecosystem file at the root of your project locally, push to git, then pull down with the ecosystem file so you won’t have to deal with merge issues when updating your web app.

Add new file:

nano ~/yourproject/ecosystem.config.js

Paste this:

module.exports = {
  apps: [
    {
      name: 'YourDomain.com',
      exec_mode: 'cluster',
      instances: 'max',
      script: 'npm', // or yarn
      args: 'start',
      env: {
        NODE_ENV: 'production',
        PORT: 3000 // or your app's port number
      },
    }
  ]
}

Install PM2 using NPM

npm install pm2 -g
pm2 start

PM2 is used in place of NPM or Yarn. Other pm2 commands

pm2 show app
pm2 status
pm2 restart app
pm2 stop app
pm2 logs
pm2 flush

To make sure app starts when reboot

pm2 startup ubuntu
pm2 save

You should now be able to access your app using your IP and port.

Install NGINX and configure

Set up a server to proxy your app running on an IP address to run on a domain name.

sudo apt install nginx
sudo nano /etc/nginx/sites-available/yourdomain.com.conf

Add the following

server {
    server_name yourdomain.com www.yourdomain.com;

    location / {
        proxy_pass http://localhost:3000; #whatever port your app runs on
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

Check NGINX config

sudo nginx -t

Create a symlink from the config file in sites-enabled to the file you just created in sites-available.

sudo ln -s /etc/nginx/sites-available/yourdomain.com.conf /etc/nginx/sites-enabled/

Restart NGINX

sudo service nginx restart

You should now be able to visit your IP with no port (port 80) and see your app. Now let’s add a domain.

Add domain in Linode

Managing DNS records on Linode is not required. You can simply copy your server’s IP address and create an A record with your domain registrar – most have a dashboard for custom DNS settings. If you’d like to use Linode’s Domain manager, implement the following.

On the Linode dashboard, go to Domains and add a domain.

Add an A record for “yourdomain.com” and for www to your droplet.

Register and/or setup domain from registrar.

Choose “Custom nameservers” and add these 3

ns1.linode.com
ns2.linode.com
ns3.linode.com
ns4.linode.com
ns5.linode.com

The domain might become available immediately, but it may take a bit to propagate.

Go to Linodes Networking > Domain manager > Add your domain name and add an A record with your server’s IP address.

Add SSL with LetsEncrypt

sudo apt-get update
sudo apt-get install python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Only valid for 90 days, test the renewal process with

sudo certbot renew --dry-run

Now visit https://yourdomain.com and you should see your Node app.

Setup ufw firewall

Now we want to setup a firewall blocking access to the ports so all port traffic is obfuscated through domain names.

sudo ufw enable
sudo ufw status
sudo ufw allow ssh (Port 22)
sudo ufw allow http (Port 80)
sudo ufw allow https (Port 443)
Node.js Deployment on Self-Managed Server