Aman Singh

Oct 21, 2025 • 6 min read

How to deploy Full Stack Web application at production on AWS

Deploying web app using AWS EC2, S3 and CloudFront

How to deploy Full Stack Web application at production on AWS

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.

Backend Deployment

Hosting it live 24/7

  1. Create a ec2 instance customise according to your need. ( i prefer ubuntu )

  2. Sign in to the instance terminal via SSH ( EC2 instance connect is easy )

  3. Install nvm

    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
  4. Load it to the bash (optional) and check it's version

    source ~/.bashrc
    nvm install --lts
    sudo apt update && sudo apt install node
    node -v && npm -v
  5. After 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 deploy
  1. run pnpm run build ( i prefer pnpm, npm is fine as well just run npm i pnpm -g )

  2. 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

  3. Install the pm2 globally

    npm i pm2 -g
    pm2 start index.js --name your-app-name
    pm2 startup

    Expected 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 list 

    Your 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-env

    Congratulations now you can access your backend on <public-ip>:3000.

    So now let's remove that ugly 3000

Hosting it on port 80

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.

  1. Update and install nginx

    sudo apt update && sudo apt install nginx
  2. Change the configurations of nginx

    sudo nano /etc/nginx/nginx.conf
    events {
     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;
     }
    }
  3. Test the config file

    sudo nginx -t

    Expected Output

    nginx: the configuration file /etc/nginx/nginx.conf syntax is ok

    nginx: configuration file /etc/nginx/nginx.conf test is successful

  4. Reload the nginx ( Restart if reload is not working )

    sudo systemctl reload nginx
    sudo systemctl restart nginx
  5. Check status

    sudo systemctl status nginx

    Expected Output

    Active: active (running)

  6. (Optional) Enable Nginx to Auto‑Start on Boot

    sudo systemctl enable nginx

    Congratulations now you've removed that ugly <ip-address>:3000 and just visit your <ip-address>

Securing your server

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.

  1. Buy a domain, refer tld for best pricing.

  2. 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

  3. Update and install the dependencies

    sudo apt update && sudo apt install certbot python3-certbot-nginx -y 
  4. Run certbot

    sudo certbot --nginx -d api.silentparcel.com
  5. Check nginx status

    sudo nginx -t
    sudo systemctl reload nginx
  6. This 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.

Frontend Deployment

So we are going to use S3 Bucket + CloudFront for the deployment of the React App.

  1. 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.production but 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 build
  1. Create a S3 bucket and upload the content of dist folder "not" the dist folder itself

  2. Go to properties

    1. Scroll to the bottom and enable the static website hosting

    2. place index.html as the root element

  3. Go to Permissions

    1. Ideally block all public access

    2. 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

  1. Select the origin as your S3 Bucket and enable use bucket endpoint and accept rest as default or customize according to your need

  2. Here you will get the domain name which you will put in nginx.conf and ARN number in Bucket Policy

  3. 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.

Join Aman on Peerlist!

Join amazing folks like Aman and thousands of other builders on Peerlist.

peerlist.io/

It’s available... this username is available! 😃

Claim your username before it's too late!

This username is already taken, you’re a little late.😐

2

10

1