A complete and straightforward guide to Node.js, Nginx, Git Deploy, and PM2 on Ubuntu.

A complete and straightforward guide to Node.js, Nginx, Git Deploy, and PM2 on Ubuntu.

Do you still need Terraform after this?

·

5 min read

Okay, I must admit that Terraform is not obsolete after this tutorial, neither Kubernetes, Azure Pipelines, Ansible, Pulumi nor Docker. I just want to show how you can deploy your first Node.js stack in seconds via SSH by having just a fresh or any existing Ubuntu server. For middle projects, this should be enough.

In the first part, we'll go through the commands required to install and configure things, then you'll get a script to automate this, to be able to perform all these steps with a single command.

The stack

1. inline Nginx

  • as we may want to have multiple Node.js applications and domains on a single server
  • static file server
  • SSL certificates

2. inline Certbot

  • we will create letsencrypt certificates

3. inline PM2

  • for managing Node.js processes and clusters. Optionally, you can enable monitoring with keymetrics

4. inline Local Git

  • to push the code, build and restart the apps

5. inline Node.js

  • n module helps to manage the nodejs versions

Install

apt update
apt install nginx nodejs npm certbot -y
npm install -g n
# update nodejs to the latest
n stable
node -v
npm -v
npm i pm2 -g
# start pm2 on OS start
pm2 startup

Git configuration

# the server directory
mkdir -p /var/www/foo
# the git repository, we will push to
git init --bare /var/www/foo.git

Then we have to create the post-receive hook - the script, which will be executed each time we push the changes to the git server. In the script we will a) copy the code to the server directory b) handle git submodules c) run build scripts d) restart the nodejs process:

cat <<EOT >> /var/www/foo.git/hooks/post-receive
#!/bin/sh
git --work-tree=/var/www/foo --git-dir=/var/www/foo.git checkout -f
cd /var/www/foo
# optionally, restore submodules
git --git-dir=/var/www/foo.git --work-tree=. submodule init
git --git-dir=/var/www/foo.git --work-tree=. submodule update
npm i
npm run build
# this will also restart foo, if already running
pm2 start foo
EOT

# allow to execute
chmod +x /var/www/foo.git/hooks/post-receive

Nginx configuration

This one we will split into two steps:

  1. create SSL certificates for the domain
  2. create the reverse proxy to the server's port

1. SSL

# to remove any default websites
rm /etc/nginx/sites-available/default

# copy-paste simple server for the letsencrypt challange
cat <<EOT >> /etc/nginx/sites-available/default
server {
    listen       80;
    server_name  foo.bar;
    root /var/www/foo;
}
EOT

# start a new server
service nginx restart

# will generate the certificates
certbot certonly --webroot -w /var/www/foo -d foo.bar --email team@foo.bar --agree-tos --no-eff-email

# now create the final server
rm /etc/nginx/sites-available/default

cat <<EOT >> /etc/nginx/sites-available/default
server {
    listen       80;
    listen       443 ssl;
    server_name  foo.bar;

    ssl_certificate  /etc/letsencrypt/live/foo.bar/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/foo.bar/privkey.pem;

    if ($scheme = http) {
        return 301 https://$server_name$request_uri;
    }
    location / {
        proxy_pass http://localhost:3000;
        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;
    }
}
EOT

service nginx restart

Project

  1. Add ecosystem.config.js file to configure the pm2
module.exports = {
    "apps": [
        {
            "name": "foo",
            "script": "index.js",
            "args": [
                "--SERVER", "--port 3000",
            ],
            "node_args": [
                "--max-old-space-size=12000"
            ],
            "timestamp": "MM-DD HH:mm Z",
            "instances": "max",
            "exec_mode": "cluster",
            "env": {}
        }
    ]
};
  1. Add the upstream repository
git init
git remote add prod root@SERVER_IP_DOMAIN:/var/www/foo.git/

You can extend and install any other software you need, which is beneficial by using plain SSH access directly to your Ubuntu instance - there are dozens of other tutorials and configurations.

Execute the commands with a single Node.js script

The commands above are simple copy-paste-enter commands, which means you can submit them one by one, but I'm using ssh2 module to submit commands over the ssh client.

I've also published the ssh2 wrapper class to make the client easier to use - sshly

import { Ssh } from 'sshly'

const script = `
apt update
---
apt install nodejs npm certbot -y
---
npm install -g n
---
n stable
---
npm i pm2 -g
---
pm2 startup
---
mkdir -p /var/www/foo
---
git init --bare /var/www/foo.git
---
cat <<EOT >> /var/www/foo.git/hooks/post-receive
#!/bin/sh
git --work-tree=/var/www/foo --git-dir=/var/www/foo.git checkout -f
cd /var/www/foo
git --git-dir=/var/www/foo.git --work-tree=. submodule init
git --git-dir=/var/www/foo.git --work-tree=. submodule update
npm i
npm run build
pm2 start foo
EOT
---
chmod +x /var/www/foo.git/hooks/post-receive
---
rm /etc/nginx/sites-available/default
---
cat <<EOT >> /etc/nginx/sites-available/default
server {
    listen       80;
    server_name  foo.bar;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/foo
        allow all;
        default_type "text/plain";
    }
}
EOT
---
service nginx restart
---
certbot certonly --webroot -w /var/www/foo -d foo.bar --email team@foo.bar --agree-tos --no-eff-email
---
rm /etc/nginx/sites-available/default
---
cat <<EOT >> /etc/nginx/sites-available/default
server {
    listen       80;
    listen       443 ssl;
    server_name  foo.bar;

    ssl_certificate  /etc/letsencrypt/live/foo.bar/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/foo.bar/privkey.pem;

    if ($scheme = http) {
        return 301 https://$server_name$request_uri;
    }
    location / {
        proxy_pass http://localhost:3000;
        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;
    }
}
EOT
---
service nginx restart
`

const client = new Ssh({
  host: 'IP',
  privateKeyPath: 'path/to/file'
});
const commands = script
  .split('---')
  .filter(x => x.trim().replace(/\r/g, ''))
  .filter(Boolean);
for (let command of commands) {
    await client.exec(command);
}

Publish the app

Push your code to the repository

git add -A
git commit -am "init"
git push -u prod master

Summary

You can create your own setup, using apache2 for example, or separate roles. After all, these are plain *nix commands with Node.js scripting, and it is a huge advantage over using some additional deployment tools or platforms.

Adjust and save the server creation script as a template, so that later you can deploy Node.js servers with ease.

Happy deploying.

Did you find this article valuable?

Support Alex Kit by becoming a sponsor. Any amount is appreciated!