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:
- Create EC2 instance
- Configure IP and domain name
- SSH into our server
- Install NGINX
- Install Node and PM2
- 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.
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".
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.
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.
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:
- Open an SSH client.
- Locate your private key file
-
Run this command, if necessary, to ensure your key is not publicly viewable.
$ chmod 400 My-ssh-key.pem
-
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
We are then going to adjust our firewall to allow http and https connections to our NGINX server.
$ sudo ufw allow 'Nginx Full'
We should then have an active web server:
$ systemctl status nginx
If we visit http://<my_ip_address>
we should see the 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
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;
}
}
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
We can go ahead and restart NGINX to make all these changes take effect.
$ sudo systemctl restart nginx
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
Change the directory owner and group:
$ sudo chown www-data:www-data /var/www/my-app
Allow the group to write to the directory with appropriate permissions:
$ sudo chmod -R 775 /var/www
Add myself to the www-data group:
$ sudo usermod -a -G www-data [my username]
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
.
-
To install
nvm
:
$ curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash $ source ~/.profile
-
Install the version of Node you want
$ nvm install 14
-
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
:
Using the following yaml file, we do the following:
- install dependencies
- run build
- run tests
- 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 }}"
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
In my case, when I run
npm run build
it outputs my built files into thedist
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)
What would the config file look like if it was being setup for a nestjs app instead of plain old nodejs
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
This was very helpful, thank you very much