DEV Community

Cover image for Deploy your Node App to EC2 with Github Actions
Andrew McCallum
Andrew McCallum

Posted on • Edited on

Deploy your Node App to EC2 with Github Actions

In this post we are going to run through how to deploy our code to AWS EC2 via Github Actions. This will include the following steps:

  1. Create EC2 instance
  2. Configure IP and domain name
  3. SSH into our server
  4. Install NGINX
  5. Install Node and PM2
  6. Deploy with Github Actions

Create EC2 instance

We are going to need to launch a new instance, create an SSH key pair and configure the correct security groups.

I wont bore you with the detail of going through the AWS EC2 creation wizard but you can go ahead and create a new EC2 instance with an Ubuntu machine image with a t2 micro and download your new keys.

Make sure you download your ssh keys and store them somewhere secure as you will need them again later.

ssh-key-selection

We have created our new instance so whilst it is booting up, we are going to configure a security group. This will allow us to accept incoming and outgoing network reqeusts on specific ports and IP addresses.

Go to the Security Groups page and then select "Create security group".

security-group-selection

We are going to allow HTTP (port 80) and HTTPS (port 443) on both our incoming and outgoing connections. This means we can access our app via http or https and we can call API's from our app on both those protocols too. Keep in mind ssh (port 22) is enabled by default.

If your app is connecting to a database, you may want to allow rules for PostgreSQL or MySQL etc.

securtiy-group-settings

Configure IP and domain name

Here we will assign an IP address to our EC2 instance and then update our domain's name server to point the domain name to our new IP.

Within the EC2 service section of AWS, navigate to Elastic IPs. Here we can click "Allocate Elastic IP address" to generate a new IP address. We can now assign that to the newly created EC2 instance we just created by clicking actions > Associate elastic IP address and selecting our EC2 instance.

alt text

Next we will point our domain name to our new IP address. In my case, my domain name uses AWS Route53 as a name server so I will navigate there and create an A record to point <my_ip_address> -> www.my-website.com.

SSH into our server

Next up we are going to access our EC2 instance via SSH. In oder to do this, we will do the following:

  1. Open an SSH client.
  2. Locate your private key file
  3. Run this command, if necessary, to ensure your key is not publicly viewable.

    $ chmod 400 My-ssh-key.pem
    
  4. Connect to your instance using its Public DNS:

    $ ssh -i "My-ssh-key.pem" ubuntu@<host>.compute.amazonaws.com
    

You should now have SSH access to your EC2 server.

Install NGINX

After ssh'ing into our server, we will need to do the following; install NGINX, configure firewall, forward web traffic ports and lastly set directory permissions.

$ sudo apt update
$ sudo apt install nginx
Enter fullscreen mode Exit fullscreen mode

We are then going to adjust our firewall to allow http and https connections to our NGINX server.

$ sudo ufw allow 'Nginx Full'
Enter fullscreen mode Exit fullscreen mode

We should then have an active web server:

$ systemctl status nginx
Enter fullscreen mode Exit fullscreen mode

nginx-status

If we visit http://<my_ip_address> we should see the NGINX welcome page.
nginx-welcome-page

We now know our web server is accepting connections on port 80. Our Node app is going to be running on port 3000 so we are going to need to forward connections from port 80 to 3000. This will be done via our NGINX config in the sites-available file.

$ sudo nano /etc/nginx/sites-available/my-app
Enter fullscreen mode Exit fullscreen mode

We are going to paste in this config:

upstream my_nodejs_upstream {
    server 127.0.0.1:3000;
    keepalive 64;
}

server {
    listen 80;
    # listen 443 ssl;

    server_name www.my-website.com; # This is the domain name we set up earlier to point to our IP address
    # ssl_certificate_key /etc/ssl/main.key;
    # ssl_certificate     /etc/ssl/main.crt;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_pass http://my_nodejs_upstream/;
        proxy_redirect off;
        proxy_read_timeout 240s;
    }
}
Enter fullscreen mode Exit fullscreen mode

This listens for traffic for www.my-website.com (this will be the domain you set up earlier to point to your IP address) on port 80 and forwards it to our node app on port 3000. As you can see, I have commented out the SSL config but if you have an SSL cert, feel free to use that instead.

We are now going to need to symlink this file into sites-enabled directory.

$ sudo ln -s /etc/nginx/sites-available/my-app /etc/nginx/sites-enabled
Enter fullscreen mode Exit fullscreen mode

We can go ahead and restart NGINX to make all these changes take effect.

$ sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

Lastly for this step, we are going to create a file for our application files to live in and set the correct permissions.

$ sudo mkdir /var/www/my-app
Enter fullscreen mode Exit fullscreen mode

Change the directory owner and group:

$ sudo chown www-data:www-data /var/www/my-app
Enter fullscreen mode Exit fullscreen mode

Allow the group to write to the directory with appropriate permissions:

$ sudo chmod -R 775 /var/www
Enter fullscreen mode Exit fullscreen mode

Add myself to the www-data group:

$ sudo usermod -a -G www-data [my username]
Enter fullscreen mode Exit fullscreen mode

We should now have NGINX installed with a config that forwards incoming requests on port 80 to port 3000 and have a directory created to contain our site files with all the correct permissions.

Install Node and PM2

We are going to install Node via NVM so that we can easily change node versions and then we will use npm to install pm2.

  1. To install nvm:

    $ curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash 
    $ source ~/.profile 
    
  2. Install the version of Node you want

    $ nvm install 14
    
  3. We should now have Node and NPM installed which we can now use to install PM2.

    $ npm install pm2 -g
    

If we had our code deployed to our server, we would then be able to start our app via PM2 but we will have to come back to this once our CI pipeline is complete.

Deploy with Github Actions

Now we can finally create a deployment pipeline in Github actions that automatically deploys our code to our new server (into the directory we created with the correct permissions). We will then move back to our ssh terminal in order to start our app.

Before we deploy anything, we are going to upload our previously downloaded ssh private key, into Github secrets so that we can reference it in our CI pipeline.

We are going to name ours SSH_PRIVATE_KEY:

github-actions-secret-settings

Using the following yaml file, we do the following:

  1. install dependencies
  2. run build
  3. run tests
  4. use rsync to copy files to our server (it will replace existing files)
name: CI

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

  workflow_dispatch:

jobs:
  build:

    runs-on: ubuntu-latest

    if: github.ref == 'refs/heads/master'

    strategy:
      matrix:
        node-version: [14.x]

    steps:
      - uses: actions/checkout@v2

      - run: npm ci
      - run: npm run build
      - run: npm run test

      - name: rsync deployments
        uses: burnett01/rsync-deployments@5.1
        with:
          switches: -avzr --delete
          path: ./*
          remote_path: /var/www/my-app/
          remote_host: <host>.compute.amazonaws.com
          remote_user: ubuntu
          remote_key: "${{ secrets.SSH_PRIVATE_KEY }}"
Enter fullscreen mode Exit fullscreen mode

Push this to our master branch in our Github repo and we should see an action run. If successful, our files should now be on our server in /var/www/my-app.

If we switch back to where we're ssh'd into our server, we can now start our app.

$ pm2 start /var/www/my-app/dist/index.js start --watch
Enter fullscreen mode Exit fullscreen mode
  • In my case, when I run npm run build it outputs my built files into the dist directory. You may have a different path to where you start your app from.

  • Also, by adding --watch we allow pm2 to restart the service whenever the files change. I.e. after each deployment.

  • If you need to set any environment variables, you can do so like so (keep in mind you will need to restart your app afterwards).

    $ export MY_ENV_VAR=foo_bar
    

We should now have successfuly setup NGINX, installed node (via NVM) and PM2 and setup a deployment pipeline in Github actions.

With any luck, our application should be available to the public via http://www.my-website.com or whatever domain name you configured.

Shout out in the comments if you have any questions or issues following this guide and I'll do my best to answer them.

Happy coding and look forward to any any feedback.

Top comments (3)

Collapse
 
salihu profile image
bitsbysalih

What would the config file look like if it was being setup for a nestjs app instead of plain old nodejs

Collapse
 
stretch0 profile image
Andrew McCallum • Edited

I haven't tried it but would have thought it would be the same. This set up should be framework agnostic and you just need to point to which ever port your node app is running on

Collapse
 
jendorski profile image
Jendorski

This was very helpful, thank you very much