Deploy NodeJS API Server to Virtual Machines

Like AWS Lightsail and Digital Ocean droplet

Photo by Matthew T Rader on Unsplash

(EDIT: Right when I was about to finish this, I felt like AWS Lightsail would be a better option for me in terms of cost and usability too, and that’s what I’m gonna explore now...)

(EDIT2: Turns out this post has been very instructive for me over at AWS Lightsail!)

(EDIT3: I finally ended up in Google Cloud Platform specifically GAE. It’s awesome. Give it a shot before AWS :)

This is just like a step-by-step cheat sheet to get your Nodejs app deployed to Digital Ocean. It’s all well documented out there, but I find it helpful if somebody would just tell me what to do, commands to run, from start to finish, to ‘deploy my nodejs’ as it’s mostly boring undifferentiator anyway. So this post serves as the ‘somebody’ that I never had.

Initial server setup

(source)

Create a new user

(Replace to your own values accordingly)

  1. ssh root@your_server_ip
  2. adduser sammy
  3. Still as a root, do usermod -aG sudo sammy

Setup firewall

  1. ufw allow OpenSSH
  2. ufw enable . Press y and enter to continue.
  3. ufw status and see that SSH connections are allowed through your firewall.

More about ufw here.

Enable ‘sammy’ to login via SSH

(At risk of stating the obvious — Replace all ‘sammy’ instances to whatever name you got!)

  1. rsync --archive --chown=sammy:sammy ~/.ssh /home/sammy

Now you can login with sammy like so:

ssh sammy@your_server_ip

and once you are in, you can do sudo if ever needed.

Simplify your login

Rather than ssh sammy@ip_address every time you wanna login to your instance, do this:

nano ~/.ssh/config

Paste this:

HostName <your_instance_ip_address>
Host <a_friendly_name>
User sammy
IdentityFile ~/.ssh/id_rsa

If you encountered Bad owner or permission... error, do this:

chmod 600 ~/.ssh/configchown $USER ~/.ssh/config

Now, to login to your instance, you just do ssh <Host> !

Manage your domain on DO

You need to move all of your DNS records like CNAME , A , etc. from whatever domain registrar you are in like ‘Namecheap’ and ‘GoDaddy’, and point nameservers to those of DO(done automatically for you when adding a new domain on DO).

Install Nginx

AFAIK, Nginx is a web server. Although it’s possible to setup Nodejs as a web server, it’s best not to do that since Node is single-threaded best for non-blocking I/O tasks, and every synchronous works and overhead related to networking and serving static assets are gonna bog down your Nodejs performance. For example, there are npm modules for implementing ‘rate limiting’, ‘cors’, ‘security headers(helmet)’ and serving static files in your Nodejs app. However, it’s actually a better practice to do all that in your Nginx!

Anyway, let’s install Nginx in your droplet:

  1. sudo apt update
  2. sudo apt install nginx

Adjust firewall to allow HTTP connections

  1. sudo ufw allow 'Nginx HTTP'
  2. sudo ufw status to confirm.

Check if Nginx is running

systemctl status nginx

Get your server’s IP address

curl -4 icanhazip.com

http://<your_server_ip> to view the nginx default homepage ensuring it’s running.

To stop and start nginx process

sudo systemctl restart nginx

To reload nginx without dropping connections

sudo systemctl reload nginx

Setup server blocks

Nginx has a default directory at /var/www/html but you wanna leave that alone as a fallback ‘ if a client request doesn’t match any other sites’.

  1. sudo mkdir -p /var/www/example.com/html (replace example.com to yours)
  2. sudo chown -R $USER:$USER /var/www/example.com/html to assign ownership to $USER .
  3. sudo chmod -R 755 /var/www/example.com to set permissions of your web roots.
  4. sudo nano /etc/nginx/sites-available/example.com to create a ‘server block’ file. Then paste this:
server {
listen 80;
listen [::]:80;

root /var/www/example.com/html;
index index.html index.htm index.nginx-debian.html;

server_name example.com www.example.com;

location / {
try_files $uri $uri/ =404;
}
}

5. sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

to ‘ enable the file by creating a link from it to the sites-enabled directory, which Nginx reads from during startup’.

6. sudo nano /etc/nginx/nginx.conf

to make some chore config changes for nginx:

Uncomment — removes # from server_names_hash_bucket_size :

http {
...
server_names_hash_bucket_size 64;
...
}

7. sudo nginx -t to ensure no syntax erors in your config file.

8. sudo systemctl restart nginx to restart nginx to enable your changes.

Enable HTTPS with Let’s Encrypt

  1. sudo add-apt-repository ppa:certbot/certbot for certbot repo.
  2. sudo apt install python-certbot-nginx to install certbot’s nginx package.
  3. sudo ufw allow 'Nginx Full' to enable https.
  4. sudo ufw delete allow 'Nginx HTTP'
  5. sudo ufw status to confirm as shown below:
OutputStatus: active

To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
Nginx Full ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
Nginx Full (v6) ALLOW Anywhere (v6)

6. sudo certbot --nginx -d example.com -d www.example.com

to obtain a SSL certificate through the certbot nginx plugin. It valids for 90 days, but it’s automatically renewed!

Select 2 when you are asked between 1 and 2, and press ‘Enter’.

Make sure you have set A records for both example.com and www.example.com in DNS records managed in Digital Ocean.

7. sudo certbot renew --dry-run

to do a dry-run on cert renewal just to be sure.

“ If you see no errors, you’re all set. When necessary, Certbot will renew your certificates and reload Nginx to pick up the changes. If the automated renewal process ever fails, Let’s Encrypt will send a message to the email you specified, warning you when your certificate is about to expire.”

Finally, if you are on AWS Lightsail, you need to enable your firewall to allow connections on 443–1) Click the menu of your instance, 2) Go to ‘Networking’ tab, 3)You will see ‘Firewall’ section. Click ‘Add another’ and select ‘HTTPS’ and the port will be set to 443 for you, and ‘Save’.

Install NodeJS

For Ubuntu and latest LTS node version(12.x right now):

curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -sudo apt-get install -y nodejs

(source)

Then, nodejs -v and npm -v to confirm.

Install build-essentials package

“ In order for some npm packages to work (those that require compiling code from source, for example)”

sudo apt install build-essential

Install PM2

PM2 is another major component of my backend. It’s a process manager for your Nodejs app. It can handle your environment variables, logging, monitoring, nodejs’s cluster mode, auto restart your nodejs process so it stays alive forever and more.

sudo npm install pm2@latest -g

git clone your Nodejs repo

I don’t know docker, Kubernetiz, docker compose and all that. I wish I knew though. I reckon would take me ages to learn. Therefore, I’m rolling with this method for now.

Just do this:

cd ~
git clone <your_github_repo> app
cd app
(NOTE: cd to your 'server' folder if necessary if you put 'client' and // 'server' in one repo)npm install

Create a config file for PM2

This file contains all your nodejs’s config for PM2. In the root of your Nodejs project, do:

nano ecosystem.config.js

Paste the content below:

module.exports = {
apps : [{
name: 'sametable',
script: 'server.js',
instances : "max",
exec_mode : "cluster",
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production'
}
}]
};

A quick note: rather than put your environment variables in places like ~/.bashrc , put them under the env property above! All your configs live in one file :)

Now we can easily startup our Nodejs app in PM2 with everything configured as dictated in the file above:

pm2 start ecosystem.config.js

To reload with 0-second downtime:

pm2 reload ecosystem.config.js

Get the application to launch on system startup

pm2 startup systemd

Your Nodejs server is not live on your domain just yet without the reverse proxy setup but we will in a second!

Troubleshooting

If you’d altered a column name but still hitting error from pg trying to access the old column, you might need to 1) pm2 delete <app_name> 2) pm2 cleardump 3) And pm2 start <app_name> again. (source)

Set PM2 to start on boot

Note, you might have to update your PM2 startup script when you update Nodejs.

sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u sammy --hp /home/sammy

Save it:

pm2 save

Finally start the service with systemctl :

sudo systemctl start pm2-sammy

Setup Nginx as Reverse Proxy server

So that netizens can access your Nodejs that is still only live on localhost in your droplet.

  1. sudo nano /etc/nginx/sites-available/example.com

Replace the location block with content below. Replace the 3000(change accordingly) port to yours.

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;
}

This configures the server to respond to requests at its root. Assuming our server is available at example.com, accessing https://example.com/ via a web browser would send the request to server.js, listening on port 3000 at localhost.

2. sudo nginx -t makes sure no syntax error.

3. sudo systemctl restart nginx

Finally, try it out by accessing your server’s URL (its public IP address or domain name).

Enable CORS in Nginx

Paste the snippet from here https://enable-cors.org/server_nginx.html in the location block like so:

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;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, PUT, DELETE, POST, OPTIONS' always;
#
}

Few things I have done differently from the snippet are:

  1. Add always to the end of everyadd_header to enable CORS in 4xx error codes(source)
  2. Add additional http methods like PUT , DELETE to ‘Access-Control-Allow-Methods’.
  3. Add ‘Access-Control-Allow-Credentials’ header if you use cookies or authorization headers for authentication(source).
add_header 'Access-Control-Allow-Credentials' true always;

4. Add any other headers if your browser complains about it in the console.

Updating your Live project

cd ~/your_repo_namegit pullnpm run build // or whatever is your npm build scriptsudo systemctl restart nginx

Self-host Redis

Whether you are on Lightsail or DO, the following guide is what you need :)

What about Database?

I went with ‘managedPostgresql database in Lightsail. It’s not much expensive relative to rolling your own in RDS or EC2 or whatever. It’s worth it coz maintaining db is not only a full-time job itself, but another boring undifferentiator from the standpoint of a modest digital product.

Then you can manage it in the good ol’ pgAdmin 💃

Rate limiting in Nginx

Setting a limit to the number of requests coming in per second helps us prevent malicious activities like brute-force password cracking or DDOS attach.

  1. Add this line in “sudo nano /etc/nginx/nginx.conf” file in the http block
limit_req_zone $binary_remote_addr zone=mylimit:10m 

2. Then add this in your domain’s location block:

location / {
limit_req zone=mylimit burst=20 nodelay;
proxy_pass http://my_upstream;
}

reference

Limit request body size

You wanna protect people from upload gigantic file to you, and this SO answer will help you https://stackoverflow.com/a/47777818/73323

Deploy SPA like React app to AWS?

‘Untitled’ by Kheoh Yee Wei