DEV Community

Cover image for Migrate Node.js App from Heroku to Digital Ocean Ubuntu 20.04 with Nginx, Pm2, SSL
Annie Taylor Chen
Annie Taylor Chen

Posted on • Updated on

Migrate Node.js App from Heroku to Digital Ocean Ubuntu 20.04 with Nginx, Pm2, SSL

Heroku has been a really great service for developers that don't want to spend time on infrastructure. I've uploaded all my toy projects there so far, because it's faster to make them available online, and I can focus more on learning and doing things I like. However, there comes a time when I want to learn more about "behind the scene", so I want to experiment with Digital Ocean, since so many people already mentioned it. Surprisingly not many tutorials were written for what I need in details, so after quite some googling, asking around and experimenting myself, I finally got it working. 😂

Are you ready?

I am ready

1. Sign up for a Digital Ocean account

First you need to sign up an account. Click Here, it is an affiliate link which will give you $200 for FREE for 60 days. How cool is that! 😉 You can use this period to experiment and learn without any cost. Note you do need to sign up with a credit card so you can be verified.

2. Set up the droplet

Next to your avatar on the top right corner, you will find a bright green button says Create. Click on it and then click droplets. And you will see this page:
Digital Ocean Droplets
Here we choose Ubuntu 20.04 LTS, Shared CPU Basic, $5/Month plan. Of course feel free to choose what suits you the best!

Scroll down and choose a data center that's near your target audiences, then select additional options, check IPv6 and monitoring.

I like to use ssh as authentication, so you can generate a key. I am sure there is a lot of tutorials online for this already. I also suggested you choose a name for your droplets. Depending on you, you can choose to have backup or not. After all that, click the green button Create Droplet. Wait a few seconds, your droplet is ready! You will be able to copy the ip address now, and from now on, we will leave Digital Ocean interface and do things in the terminal.

3. Login and update your ubuntu system

Now open a terminal and type

ssh root@youripaddress
Enter fullscreen mode Exit fullscreen mode

Note normally you shouldn't do stuff with root. More power more responsibility! You might mess up things by accident, so it's better to create a user. For simplicity I will continue with root for now. Note when you login as user later, you gotta make sure to give user enough right to write some files, such as some nginx conf files. I will paste some read further links at the end.

It will ask you if you want to continue, then type yes. Then you will see root@DropletName:~#.

Let's update now. Upgrade is optional. Some people don't do it at all, it's up to you.

sudo apt update
sudo apt upgrade
Enter fullscreen mode Exit fullscreen mode

4. Let's install Node.js

In the terminal type: (This is the latest version so far but you can replace 14 with 12 if you want stable version)

curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh
sudo bash nodesource_setup.sh
sudo apt install nodejs
node -v
Enter fullscreen mode Exit fullscreen mode

When you see output of node version number like

v14.6.0
Enter fullscreen mode Exit fullscreen mode

We have successfully installed node on our system.YEAH!

5. Let's move our repo to our droplet

Since we already have a node.js app on github, we can easily git clone it here. Let's first create a folder called Apps for all our apps.

mkdir apps
cd apps 
git clone git clone https://github.com/username/repo.git
ls -a
Enter fullscreen mode Exit fullscreen mode

Now you should see something like this

.  ..  repo
Enter fullscreen mode Exit fullscreen mode

If the output is your repo's name, we successfully copied our repo here.

6. Install dependencies and test run

Now first let's go into your repo here

cd repo
npm install
npm start npm 
Enter fullscreen mode Exit fullscreen mode

Now go to the ipaddress:3000 (or whatever port you're running on)

can you see your app running? If so, well done! 👍 Press Ctrl + C to stop as we still have a lot to do.

7. Set up Pm2 to keep your app running

Now let's install pm2 globally. You can read more about pm2 here.

sudo npm install pm2@latest -g
pm2 start app 
Enter fullscreen mode Exit fullscreen mode

For instance my app's file is in src, so I cd to my apps/repo then run

pm2 start src/index.js
Enter fullscreen mode Exit fullscreen mode

Something like this shows up, and you should still be able to see your ipaddress:3000 (or whatever port you use) in your browser to see your app running.
pm2 run successfully

To make sure your app starts when reboot you can type:

pm2 startup ubuntu
Enter fullscreen mode Exit fullscreen mode

8. Set up firewall to block port

When you type

sudo ufw status
Enter fullscreen mode Exit fullscreen mode

You will probably see Status:inactive. Now let's enable it

sudo ufw enable
sudo ufw status
Enter fullscreen mode Exit fullscreen mode

Now it should say Status: active. Let's allow a few ports.

sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
Enter fullscreen mode Exit fullscreen mode

ssh is port 22, http port 80, https port 443.

9. Set up and configure Nginx as reverse proxy

sudo apt update
sudo apt install nginx
sudo ufw allow 'Nginx HTTP'
sudo ufw status
Enter fullscreen mode Exit fullscreen mode

Now you should see something like this:
ufw status

Now we can go to the http://ipaddress, (note we don't need to type port any more) and you shall see the following.
Alt Text
That means you're doing right so far!

Now here comes the tricky part. Since I plan to host more toy projects within one droplets, which will also point to different domain names (or subdomain names). We'd better create something extra, instead of modifying the default.

Let's say we're going to serve the current app at a.example.com. First let's create something like this:

sudo mkdir -p /var/www/a.example.com/html
nano /var/www/a.example.com/html/index.html
Enter fullscreen mode Exit fullscreen mode

Then we paste those simple html here, it won't show up so don't need to make it pretty.

<html>
    <head>
        <title>Welcome to a.example.com!</title>
    </head>
    <body>
        <h1>Success! a.example.com server block is working!</h1>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

To save you press Ctrl + X, yes, then press enter. Now we have this file to refer to later in our another conf file.

First let's create something like :

sudo nano /etc/nginx/sites-available/a.example.com
Enter fullscreen mode Exit fullscreen mode

copy and paste the following to that

 server {
     listen 80;
     listen [::]:80;

       server_name a.example.com;

       root /var/www/a.example.com/html;
       index index.html;

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

Enter fullscreen mode Exit fullscreen mode

Ctrl + X, yes, press enter.

Now that we have our server block file, we need to enable it. We can do this by creating symbolic link from this file to the sites-enabled directory, which Nginx reads from during startup. Later when we add more apps and more domains names, we can copy the same steps.

We can create these links by typing:

sudo ln -s /etc/nginx/sites-available/a.example.com /etc/nginx/sites-enabled/
Enter fullscreen mode Exit fullscreen mode

To avoid a possible hash bucket memory problem that can arise from adding additional server names, it is necessary to adjust a single value in the /etc/nginx/nginx.conf file. Open the file:

sudo nano /etc/nginx/nginx.conf
Enter fullscreen mode Exit fullscreen mode

Find the server_names_hash_bucket_size 64 directive and remove the # symbol to uncomment the line.

Now let's see if our setting is ok. Type

sudo nginx -t
Enter fullscreen mode Exit fullscreen mode

If you see those lines, then you're successful.

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Enter fullscreen mode Exit fullscreen mode

Now we can restart Nginx.

sudo service nginx restart
Enter fullscreen mode Exit fullscreen mode

10. Set your domain/subdomain name

You need to set an A record. Since I manage my domain name at Netlify, it's relatively easy to do. For instance, I want the domain to sound like meowlo.annietaylorchen.com, so I set it up as below.
set subdomain
Note it can take up to 24 to 28 hours for your domain name to propagate, so you need to be a little patient at this point.

Use whatsmydns to check if your domain name is available or not.

11. Set up environment variables

If you're using some api keys, we need to set up environment variables. Since I already use dotenv in my node.js app, I will do as follow. I don't know whether this is the best practice, but it somehow worked for me.

Since we copied our git from github and naturally we didn't push our .env file there, now on our Digital Ocean droplet we also don't have that. But we need to use it when running our apps.

Now cd to your app folder, and type

touch .env
nano .env
Enter fullscreen mode Exit fullscreen mode

Now you copy your api keys like

API_KEY=49upogjergeu
Enter fullscreen mode Exit fullscreen mode

then Ctrl + X, yes, press enter to save. Now you type

cat .env
Enter fullscreen mode Exit fullscreen mode

You should be able to see them showing up in your terminal. Ok, now make sure when you use dotenv you write something like

if (process.env.NODE_ENV == 'production'){
    require('dotenv').config()
}
Enter fullscreen mode Exit fullscreen mode

and in your npm script it's sth like:

"start": "NODE_ENV=production node src/index.js",
Enter fullscreen mode Exit fullscreen mode

Now let's inform pm2 about this

NODE_ENV=production pm2 restart src/index.js --update-env
Enter fullscreen mode Exit fullscreen mode

Now your apis should be working! 😺

12. Add SSL with LetsEncrypt

Now let's go back to our home directory.

cd
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d a.example.com
sudo certbot renew --dry-run
Enter fullscreen mode Exit fullscreen mode

Simply follow the instruction they prompt in the terminal. If you don't see any error, we're all set. Now refresh your browser, you should see your a.example.com serving your apps, api working, and it's secure too! Pfew! That's a lot, I know! Congrats if you make it to the end. Let's high five!🙌

happy dance

Last but not least, I have made it as short as possible. There are quite some details I didn't cover, but I think if you have time you should read further later.

  1. Full Node.js Deployment - NGINX, SSL With Lets Encrypt by Brad Traversy - note this is not the latest version I used, but many concepts are the same.
  2. How To Install Git on Ubuntu 20.04 - ubuntu usually comes with git already, but you can set up with your own name and email etc.
  3. How To Set Up Automatic Deployment with Git with a VPS - teaches you how to commit from local repo through git, and how to set up beta version
  4. How To Set Up a Node.js Application for Production on Ubuntu 20.04 - general guide for node.js app
  5. How To Install Nginx on Ubuntu 20.04 - general guide for nginx
  6. How To Set Up Nginx Server Blocks (Virtual Hosts) on Ubuntu 16.04 - if you want to serve different sites on the same droplets this is useful
  7. How To Secure Nginx with Let's Encrypt on Ubuntu 20.04 - how to add SSL to your site
  8. How To Install and Use PostgreSQL on Ubuntu 20.04 - my simple node.js app doesn't require a database, but you will probably need one

Top comments (0)