How I Set Up a Server To Host Multiple Node.js Projects & Save $54+ Per-App Annually vs Heroku

Photo by Joshua Aragon on Unsplash

What and Why

I have built several small lightly used Node.js projects on Heroku. I want several of them to not have the limitations of a free server, namely the fact it sleeps after 30 minutes & the ability to have free SSL certificates. There are a few other gotchas with Heroku in general. According to their pricing, I’d need to get, at minimum, a ‘Hobby’ server, one for every app. Each one costs $7 a month plus taxes which equates to more than $84 a year for each app.

  • Apps will need to support a single Node.js version, at the time of writing it is 12.18.3. To check your version once the server is setup simply run node -v
  • You need to know the difference between IPv4 and IPv6
  • You need to know how to set up DNS. The method is different for each DNS host
  • Each app needs to have its own apex domain, subdomain or combinations thereof assigned to it
  • All apps should be listening for the PORT environment variable
  • In the instructions, wherever you see john replace it with the non-root user you’ll be creating and using

Deploy a server and initially set it up

Steps to deploy a new VULTR Cloud Compute Server:

  1. Login to VULTR
  2. In the top right corner, click the circle with a plus sign then ‘Deploy New Serve’
  3. Because we can always scale up I personally selected the cheapest type of server Cloud Compute server
  4. Choose a datacenter region that makes sense for your region
  5. Make sure Ubuntu is selected with ‘20.04 (LTS) x64’
  6. For the server size, I chose the cheapest which is $2.50 a month. If I ever need more resources, I can scale up.
  7. Optionally, but suggested, enable auto backups (additional cost)
  8. If you know about and have an SSH key, use that for auth. Either way, you will be able to find root’s password where you can manage the server on VULTR’s site.
  9. Give the droplet a clever hostname
  10. Optionally give the server an alternate label to find it on VULTR’s site
  11. Today we are only deploying 1 server
  12. Click ‘Deploy Now’
  13. Log into your server using root over SSH (if you don’t know how see this article). You will find the IP address info when you go to manage your Cloud Compute server’s settings on VULTR’s site.
  14. To update the list of packages run apt update
  15. To upgrade out of date items run apt upgrade

Setup a non-root user with sudo privileges

  1. Log into root via SSH if you are not already connected
  2. Let’s set a temp variable to make the next bunch of steps newuser=john
  3. Run the command adduser $newuser and fill in the answers, if you want
  4. Run the command usermod -aG sudo $newuser

Optional 1 — If you wish to secure the server via SSH keys

  1. Run the following set of commands to set up SSH for the new user account
mkdir -p /home/$newuser/.ssh
touch /home/$newuser/.ssh/authorized_keys
chmod -R go= /home/$newuser/.ssh
chown -R $newuser:$newuser /home/$newuser/.ssh
nano /home/$newuser/.ssh/authorized_keys
  1. Find the public SSH key that you wish to use and paste it into the SSH session where nano is running, it should only take up one line
  2. If you want multiple keys for the account, optionally add additional keys, 1 per line
  3. Press ‘ctrl-x’ to exit, then ‘Y’ to save and then finally press enter to accept the file name

Optional 2 — If you want to lock down the server and disallow passwords to authenticate an SSH session

  1. Run sudo nano /etc/ssh/sshd_config
  2. Find the line containing ‘PasswordAuthentication’. If there is a ‘#’ before it, remove it, then make sure the line says PasswordAuthentication no
  3. Press ‘ctrl-x’ to exit, ‘Y’ to save, then ‘enter’ to keep the existing filename
  4. Lastly, run sudo systemctl restart ssh

Setup initial firewall

  1. Run the following command to enable the firewall ufw allow OpenSSH
  2. Run the following command to permit only SSH connections ufw enable
  3. Log off of the root account by running exit. From here on in we will be using the account, john

Setup Nginx

  1. Connect to the server again using ssh and the account john
  2. Run the following sudo apt update to update the apt program, you will be asked for a password most likely, enter it
  3. Run the following sudo apt install nginx to install nginx
  4. Run the following sudo ufw allow 'Nginx Full' to enable HTTP/HTTPS traffic (ports 80 and 443)
  5. To verify everything is working navigate, from your browser to http://your_server_ip (HTTP, not HTTPS) you should get a standard ‘welcome to nginx’ page
  6. To fix a possible memory problem, lets set the hash bucket size by running sudo nano /etc/nginx/nginx.conf
  7. Look for the ‘server_names_hash_bucket_size’ directive and remove the ‘#’ from the line
  8. Exit by pressing ‘ctrl-x’, save by pressing ‘y’, accept file name by pressing ‘enter’
  9. Validate the config file by running sudo nginx -t you should get the following output if all is well:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
  1. Restart nginx by running sudo systemctl restart nginx

Prepare server to use Let’s Encrypt SSL Certificates

  1. To install certbot and it’s Nginx plugin run sudo apt install certbot python3-certbot-nginx

Set up Node.js’s latest LTS version

  1. Update apt-get with the correct package by running curl -sL | sudo -E bash -
  2. Let’s install it by running sudo apt-get install -y nodejs
  3. Let’s install a build tool by running sudo apt-get install -y build-essential
  4. Let’s indicate that, by default, node apps should be running in production by doing the following
  5. Run sudo nano /etc/environment
  6. Add a new line containing NODE_ENV="production"
  7. Save the file by ‘ctrl-x’ , ‘y’, and ‘enter’
  8. Load the new environment variable by running source /etc/environment

Install PM2, a process manager for Node.js

  1. Run the following command to install PM2 sudo npm install pm2@latest -g
  2. Acquire a command to setup PM2 to run at startup by running pm2 startup systemd
  3. The last line of the output for the prior command, will be another command, run it
  4. Kick off the initial run of the PM2 service by running sudo systemctl start pm2-john (replacing john)
  5. Optional but recommended if you intend to have logging of your apps, we can set up log rotation. This will save 1 log file per period (defaults to daily, or when the file is larger than 10MB), and keep a maximum number of log files (which defaults to 30).
  6. Let’s install pm2-logrotate by running pm2 install pm2-logrotate
  7. Optionally change the defaults of pm2-logrotate, see the instructions here

Set up a new app with its corresponding domains

Set up DNS

  1. Go to your DNS server and set up an A record pointing to the IPv4 address for each domain you wish to be serviced by this app
  2. On your DNS server, set up an AAAA record pointing to the IPv6 address for each domain you wish to be serviced by this app

Using git, copy over and set up the project

  1. Use git to clone the repo into an appropriate folder, replace repo with the git address, and replace with your chosen domain. sudo git clone repo /var/www/
  2. Take ownership of the folder by running sudo chown -R $USER:$USER /var/www/
  3. Change directory cd /var/www/
  4. If you need to npm install or run build commands, now is the time

Set up PM2 to launch, manage, and provide environment variables to the app

  1. Head back to the home folder by running cd ~
  2. Let’s create an ecosystem file, run nano
  3. Copy the below text into the SSH session
  1. Replace demo with a proper name
  2. Replace the script path with the fully qualified path to the entry of your program.
  3. Change the PORT to the correct port you wish to use, keeping in mind each app should have a unique port.
  4. Within the env object, each pair will be fed into your program as an environment variables overriding any system variables if they match. Feel free to add additional ones as you need, they are unique to this app.
  5. If you wish to log to disk, uncomment the log_file line, changing both john and the Optionally uncomment the time line to have the logs prepended with a timestamp.
  6. Press ‘ctrl-x’ to exit, ‘y’ to save, ‘enter’ to accept the name
  7. If opting to log to disk, let’s create the folder for it by running mkdir -p ~/applogs/
  8. To start the app for the first time run pm2 start
  9. Let’s save the PM2 process list and corresponding environments so that on server startup, it’ll restart all the apps including the latest app we just added. Run pm2 save
  • Stop the app: pm2 stop
  • Restart the app: pm2 restart
  • Monitor your apps: pm2 monit

Set up an nginx server block for this app

  1. To create and edit the server block file, run sudo nano /etc/nginx/sites-available/
  2. Paste the following into you shell session
server {
listen 80;
listen [::]:80;
server_name;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;
  1. Replace the (there are two domains in this example listed, you can do one, two or more on the server_name line)
  2. If necessary change the port from 3000 to the correct port the app is listening on
  3. Next, let’s enable the file by creating a link from it to the sites-enabled directory, which Nginx reads from during startup by running sudo ln -s /etc/nginx/sites-available/ /etc/nginx/sites-enabled/
  4. Let’s check our work in the nginx configs by running sudo nginx -t
  5. If all is well, run sudo systemctl restart nginx

Set up SSL certificates for your domain

  1. Let’s run certbot with the –nginx plugin, using -d to specify the domain names we’d like the certificate to be valid for by running sudo certbot --nginx -d -d
  2. Answer the question about how you want to handle non-SSL traffic, I recommend ‘Redirect’ to redirect non-secure HTTP traffic to HTTPS.
  3. The site should now be alive at when you navigate there, you should be redirected to


We now have a server with one or more NodeJS apps running on it. We configured the server with a node process manager (PM2) to take care of starting and stopping Node.js apps as needed. We are now resilient against app crashes, it’ll be restarted automatically. When the server restarts, our apps will start back up automatically as well. We set up a reverse proxy using nginx so that each app will respond to its unique domain name(s). Lastly, we saved money vs using Heroku.



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store