Deploying web app using AWS EC2, S3 and CloudFront

I was working on a product and it was going smooth while i was on local development mode and realised that i have to use AWS to deploy this app, and the fun part is i never used AWS before so had to figure out everything by myself
It was a monorepo ( had two folders FE && BE ) the FE was written in ReactJS while the BE in ExpressJS.
So for deploying i used AWS services like EC2, S3, CloudFront, Certificate Manager. So first let's talk about the easiest part in the deployment.
Create a ec2 instance customise according to your need. ( i prefer ubuntu )
Sign in to the instance terminal via SSH ( EC2 instance connect is easy )
Install nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bashLoad it to the bash (optional) and check it's version
source ~/.bashrcnvm install --ltssudo apt update && sudo apt install nodenode -v && npm -vAfter installing the node clone your repo and go to the backend folder
PS : Config your .env in the backend and load it on the top of index.ts file
import dotenv from "dotenv"; import path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../.env') }); // Since after compiling the index.ts will go in dist/index.js so we have to tell the code that .env is in the previous dir. In your case it might be something else
Since i was using Prisma ORM so we need to fetch schema to avoid build errors
npx prisma generate
npx prisma migrate deployrun pnpm run build ( i prefer pnpm, npm is fine as well just run npm i pnpm -g )
Since we need to run the code 24/7 we need to setup for that as well. Most comfortable way to is to use pm2
Install the pm2 globally
npm i pm2 -gpm2 start index.js --name your-app-namepm2 startupExpected Output
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u ubuntu --hp /home/ubuntu
pm2 save
pm2 list
pm2 log <your-app-name>You can check if your instance is running properly
sudo reboot pm2 listYour application must be in the list
If you made some changes in files and .env you can just restart it
pm2 restart your-app-name --update-envCongratulations now you can access your backend on <public-ip>:3000.
So now let's remove that ugly 3000
There are many ports but in order to hide that ugly port sequence. We have two special ports 80(http) and 443(https).
We will be using the most popular FOSS nginx for reverse proxy.
Update and install nginx
sudo apt update && sudo apt install nginxChange the configurations of nginx
sudo nano /etc/nginx/nginx.confevents {
worker_connections 1024;
}
http {
server {
listen 80;
server_name something.cloudfront.net silentparcel.com www.silentparcel.com; # you can put multiple domain to redirect at same port like 3000 in this case
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;
# Proxy timeouts and buffering
proxy_read_timeout 90;
proxy_buffering on;
proxy_buffers 8 16k;
proxy_buffer_size 16k;
}
access_log /var/log/nginx/tractual_access.log;
error_log /var/log/nginx/tractual_error.log warn;
}
}Test the config file
sudo nginx -tExpected Output
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Reload the nginx ( Restart if reload is not working )
sudo systemctl reload nginxsudo systemctl restart nginxCheck status
sudo systemctl status nginxExpected Output
Active: active (running)
(Optional) Enable Nginx to Auto‑Start on Boot
sudo systemctl enable nginxCongratulations now you've removed that ugly <ip-address>:3000 and just visit your <ip-address>
In the last section we had successfully removed the ":3000" and you can visit your address without it.
Sometime keeping only http do the work but it's advisable to have a https secured connection.
Buy a domain, refer tld for best pricing.
Create a A record from your DNS settings with name as ( let's say 'api' here) and address as your public IPv4 of your instance.
This might take time to propagate properly
Update and install the dependencies
sudo apt update && sudo apt install certbot python3-certbot-nginx -y Run certbot
sudo certbot --nginx -d api.silentparcel.comCheck nginx status
sudo nginx -t
sudo systemctl reload nginxThis is how the nginx.conf will look like
events {
worker_connections 1024;
}
http {
server {
server_name api.silentparcel.com;
location /.well-known/acme-challenge/ {
root /var/www/html/;
allow all;
}
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;
proxy_read_timeout 90;
proxy_buffering on;
proxy_buffers 8 16k;
proxy_buffer_size 16k;
}
access_log /var/log/nginx/api_silentparcel_access.log;
error_log /var/log/nginx/api_silentparcel_error.log warn;
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/api.silentparcel.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/api.silentparcel.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = api.silentparcel.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name api.silentparcel.com;
return 404; # managed by Certbot
}}Congratulations you have successfully deployed and attained "https".
Though this was the easiest step yet i took most of my time in it. So don't worry if you are not able to do in one go.
So we are going to use S3 Bucket + CloudFront for the deployment of the React App.
In order to deploy a successfully react app you need to compile the entire react-app. In the production to keep a endpoints for each api to reduce the involvement of the .env variable since we will be deploying a static file.
Still if you want to add a env variable in it you can follow the below example snippet to add a var while building the app.
You can also use
.env.productionbut since i had one variable i didn't implemented that
REACT_APP_VITE_BACKEND_URL=https://api.silentparcel.com \
REACT_APP_GEMINI_API=https://api.gemini.com \
REACT_APP_RESEND_API=https://api.resend.com \ pnpm run build
---------------------------------------------------
REACT_APP_VITE_BACKEND_URL=https://api.silentparcel.com pnpm run buildCreate a S3 bucket and upload the content of dist folder "not" the dist folder itself
Go to properties
Scroll to the bottom and enable the static website hosting
place index.html as the root element
Go to Permissions
Ideally block all public access
Create a Bucket Policy
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<your-bucket-name>/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::numerical:string/string"
}
}
}
]
}Now we will configure CloudFront
Select the origin as your S3 Bucket and enable use bucket endpoint and accept rest as default or customize according to your need
Here you will get the domain name which you will put in nginx.conf and ARN number in Bucket Policy
Go in origins tab and add the EC2 instance which we created earlier
Congratulations you have successfully deployed a Full Stack MERN application.
You can add your own domain in alternate domain names section. It is pretty simple and self explanatory and you can pull that off easily.
2
10
1