Introduction
First of all, this is a verrrrrryyyyy loooooooooonnng epistle. Feel free to skim through to get the gist, and then come back again when you need to use it for a project.
In this guide, I’ll be as detailed as possible guiding you step by step on setting up your EC2 Instance, your code repository up to testing your deployment. Bookmark! Let’s go!
Setting Up an EC2 Instance on AWS.
Navigate to the EC2 dashboard.
Click on “Launch Instance” to create a new EC2 instance.
Choose an Amazon Machine Image (AMI) (We’ll use an Ubuntu Server AMI for this setup).
Select an instance type based on your resource needs (e.g., t2.micro for testing or t2.small/medium for production).
Create a key-value pair (use RSA) and store the file in a secure location on your PC. You’ll be needing it for SSH Access later.
Select appropriate network settings for your instance.
You can choose to allow access to your instance from anywhere (this is not recommended though – use your IP Address, or add a custom IP address from whence you can ssh into the instance).
- Allow SSH access (port 22) from your IP address.
- Allow HTTP (port 80) and HTTPS (port 443) from anywhere.
Set up storage as needed for your project: Free-tier eligible instances can get up to 30GB storage. So let’s just stick with 8GB for now.
Advanced details? There’s a bunch of stuff under that section. You can read up on that to know if you would want to modify any of the default settings.
Review (Some weird JSON stuff, haha) and then Launch your Instance
Click on Connect To Your Instance for the Next Steps
Take note of your public IP and username (ubuntu), you can use the ssh command to get into your instance via your PC:
ssh -i /path/to/your-key-pair.pem ubuntu@your-instance-public-ip
Or just use the browser-based console by clicking on Connect To Instance
Browser-based Console. Yaaaaay! We’re in.
Setting Up the Environment
We’ll make the most of the web console provided to us on the AWS platform to access our launched instance.
*Update the system packages *
sudo apt update && sudo apt upgrade -y
Install Node.js and Node Package Manager (npm)
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
*Install Git for Version control – I mean, why not! *
sudo apt install git -y
Install PM2 globally (A process manager for managing multiple Nodejs application on the same machine).
sudo npm install pm2@latest -g
Install and enable Nginix for reverse proxying
sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
Create a directory on the home (~) path where we’ll house our different app source code.
mkdir ~/apps
cd ~/apps
The steps above will help us prepare our instance with the necessary software to run the Node.js applications and set up a reverse proxy for routing requests. Now, to the next step
Configuring Nginx as a Reverse Proxy
Remember, web requests are sent over HTTP/HTTPS which connect to your port 80/8080/443. With your node applications running on other non-web-ports, we’ll need a web server like Nginix that will “forward” all HTTP requests that hits our server to the appropriate ports where the application we want to interact with is running – this is a better approach than exposing your other ports directly to the internet.
Create a new Nginix Configuration file
Let’s create a file where we’ll store the configuration for proxying all requests to our applications using the server name (your domain or you can use your instance’s Public IP Address in the meantime).
sudo nano /etc/nginx/sites-available/nodejs-apps
server {
listen 80;
# This server block will server our apps on the same domain
# If you have multiple apps serving multiple domains you can
# create a new server block for that domain and the set up is
# pretty much the same.
server_name 00.00.99.40; # Your domain or public IP here
location /app1 {
proxy_pass http://localhost:3001/;
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;
}
location /app2 {
proxy_pass http://localhost:3002/;
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;
}
}
Paste the configuration above and modify accordingly. You can delete the default configuration file in /etc/nginx/sites-enabled/default
so that it doesn’t take precedence over your configuration or modify it accordingly.
Note: make sure the trailing / after the http://localhost:3001/
remains. This instructs nginx to strip off the /app1 and /app2 for example when forwarding the route to your node server.
Enable the new configuration file
sudo ln -s /etc/nginx/sites-available/nodejs-apps /etc/nginx/sites-enabled/
Test the configuration to ensure the your settings are valid.
sudo nginx -t
sudo systemctl reload nginx
systemctl status nginx
And we’re all set. Let’s get our code in here.
Set up GitHub repositories and clone your applications
For each of your Node.js applications, create a separate GitHub repository if you haven’t already. I have created a very simple NodeJS Application that we’ll be deploying for testing purposes.
You can find them here:
- https://github.com/Cre8steveDev/Deploying_Multiple_Node_App_001
- https://github.com/Cre8steveDev/Deploying_Multiple_Node_App_002
Navigate to the apps directory we created earlier in ~apps
cd ~/apps
Clone each of your repositories (First add your SSH Keys on Github)
Generate an ssh key to set up SSH Authentication for your ec2 instance on Github.
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
cat ~/.ssh/id_rsa.pub
Just hit enter for the prompts. Copy the code displayed by the cat command, and add it to your SSH Keys on github.
git clone git@github.com:Cre8steveDev/Deploying_Multiple_Node_App_001.git
git clone git@github.com:Cre8steveDev/Deploying_Multiple_Node_App_002.git
For each application, navigate to its directory and install dependencies
cd Deploying_Multiple_Node_App_001
npm install
cd ../Deploying_Multiple_Node_App_001
npm install
# If you're deploying more apps, do the same for all
Create a .env
file for each application if needed, to store environment-specific variables. In our case, we’re storing the PORT number in the .env
, so we’ll do that for each of the cloned directory:
echo "PORT=3001" > .env
# and the appropriate port for the others
Test each application to ensure it runs correctly (If you’re following the sample server code provided, you can build the project with):
npm run build # Convert typescript to js
npm run start # Run the server from the dist directory
Set up PM2 to manage your Node.js applications
Create a PM2 ecosystem file to manage all your applications
cd ~/apps
pm2 ecosystem generate nodejs-apps
Open the generated ecosystem.config.js
using your favourite editor: nano or vim or emacs – no vscode here haha
Here’s an example configuration for serving both our applications. It’s quite explanatory.
module.exports = {
apps: [
{
name: "Node_App_001",
script: "./Deploying_Multiple_Node_App_001/dist/index.js",
env: {
NODE_ENV: "production",
PORT: 3001
}
},
{
name: "Node_App_002",
script: "./Deploying_Multiple_Node_App_001/dist/index.js",
env: {
NODE_ENV: "production",
PORT: 3002
}
},
]
};
Now, start all your applications using PM2 command, on the ~/apps
directory
pm2 start ecosystem.config.js
Set up PM2 to start on system boot
pm2 startup systemd
Follow the instructions provided by the command to complete the setup – basically, copy the bash command, that is displayed, paste and hit enter.
Save the current PM2 process list
pm2 save
This step ensures that your Node.js applications are managed efficiently and will restart automatically if the EC2 instance reboots.
Set up automatic deployment using GitHub Actions
Step 1: In each of your GitHub repositories, create a new directory for GitHub Actions – You can do this via your code editor (Not on your server. Your local machine – where you work on your codebase ):
mkdir -p .github/workflows
bash
Create a deployment workflow file in each repository
nano .github/workflows/deploy.yml
# You can use your editor of course.
Step 3: Add the following content to the deploy.yml file (Make sure to customize for each application/repository – paying special attention to the last 5 lines where you define the name of the directory for each application and the command to run after each pull of new updates from GitHub). The file below is for the App 01. modify accordingly for all the repository you intend to setup GitHub actions on.
name: Deploy to EC2 for Application Server 01
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to EC2 for Application Server 01
env:
PRIVATE_KEY: ${{ secrets.EC2_PRIVATE_KEY }}
HOST: ${{ secrets.EC2_HOST }}
USER: ubuntu
run: |
echo "$PRIVATE_KEY" > private_key && chmod 600 private_key
ssh -o StrictHostKeyChecking=no -i private_key ${USER}@${HOST} '
cd ~/apps/Deploying_Multiple_Node_App_001 &&
git pull origin main &&
npm install && npm run build &&
pm2 stop Node_App_001 || pm2 delete Node_App_001; pm2 start npm --name "Node_App_001" -- start && pm2 save
'
pm2 restart Node_App_001
the Node_App_001
is the name of the app we created in the ecosystem.config.js, remember?
The secrets.EC2_PRIVATE_KEY
is the key-pair .pem
file we created earlier when we launched our instance. Remember, I asked you to keep it secure – If you didn’t, you can create a new key-value pair on your instance console and add it on github. DO NOT Paste it directly in the deploy.yml file.
In your GitHub repository settings, go to “Secrets and variables” > “Actions” and add two new secrets:
EC2_PRIVATE_KEY
: The content of your EC2 instance’s private key file (copy and paste everything in the value field)EC2_HOST
: Your EC2 instance’s public IP address or domain name
View the actions tab on the individual repositories to confirm
Now push these new configuration to GitHub. This step sets up automatic deployment for your applications whenever you push changes to the main branch of your GitHub repositories. The script within the run
of your deploy.yml will restart the appropriate node process of your app and restart it – make sure you update it accordingly for your use case.
Update your codebase with new code, commit and push to the individual repository and check it out on your EC2 Instance to confirm that it pulled the new changes
Check the status of the github action on github
Checkout both apps running live (I will delete the instance after this tutorial – but you can see that they’re being served based on the url /app1
and /app2
).
Take a break!
Phew! If you’ve made it this far, wow! If you’ve not had any errors, wow! wow! Haha, but I’ve made sure every step is covered to the letter.
Set up SSL/TLS for your domain using Let’s Encrypt and Certbot
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d your-domain.com
Unfortunately, I do not have an active/free domain to show screenshots for this step. However, the process is quite easy. Just use your domain, follow the prompt and the ssl certificate will be generated for you (You’ll have to renew every 90 days or so – or you can set up a script to help with that).
After generating the ssl certificate, update your nginx configuration , basically telling nginx to redirect every http call to https. This is what it would look like:
server {
listen 80;
server_name your_domain.com; # Replace with your domain or IP
return 301 https://$server_name$request_uri; # Redirect HTTP to HTTPS
}
server {
listen 443 ssl;
server_name your_domain.com; # Replace with your domain or IP
ssl_certificate /path/to/your/fullchain.pem; # Path to your SSL certificate
ssl_certificate_key /path/to/your/privkey.pem; # Path to your SSL private key
# Additional recommended SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
location /app1 {
proxy_pass http://localhost:3001/;
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;
#return 200 'Nginx is handling /app1 correctly';
}
location /app2 {
proxy_pass http://localhost:3002/;
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;
}
}
Test the configuration to ensure the your settings are valid.
sudo nginx -t
sudo systemctl reload nginx
systemctl status nginx
And that’s a wrap!
Conclusion
Thanks for reading through. I hope you find the guide helpful as you explore this adventurous ride in deploying your projects on your own VPS (AWS EC2 instance in this case) – the same idea can be implemented on any self-hosted server too. Cheers, and have fun building!
Additional Resources:
- PM2 Documentation – https://www.npmjs.com/package/pm2
- Github Actions Workflow – https://docs.github.com/en/actions/writing-workflows
- Register for AWS Free Tier Services – https://aws.amazon.com/free/
- Nginix Documentation: https://nginx.org/en/docs/beginners_guide.html
Take Note
- Free Tier Limits: The AWS Free Tier offers 750 hours of t2.micro or t3.micro instances each month for the first 12 months after you sign up. This means you can run one instance continuously for the entire month without incurring charges, or you can run multiple instances as long as the total running hours do not exceed 750 (This is fine for your prototyping/hosting side projects. Hence this guide to maximize your ec2 resource).
- Instance Types: Only specific instance types (t2.micro and t3.micro) qualify for the Free Tier. If you use a different instance type, you will incur charges.
- Region Availability: Ensure that the instance types you want to use are available in your selected AWS region, as Free Tier eligibility can vary by region.
Source link
lol