How to Deploy a MERN App on AWS

21 Oct 2025

Introduction

I was working on a product that ran smoothly in local development mode, then realized I needed to deploy it on AWS. The fun part? I'd never used AWS before, so I had to figure everything out myself.

The project was a monorepo with two folders:

  • Frontend (FE): ReactJS
  • Backend (BE): ExpressJS

For deployment, I used AWS services: EC2, S3, CloudFront, and Certificate Manager. Let's start with the backend deployment.

Backend Deployment

Hosting it Live 24/7

1. Create and Connect to an EC2 Instance

Create an EC2 instance and customize according to your needs (I prefer Ubuntu). Sign in to the instance terminal via SSH (EC2 Instance Connect is easiest).

2. Install Node.js via NVM

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install --lts
sudo apt update && sudo apt install node
node -v && npm -v

3. Setup Environment Variables

Clone your repo and configure the .env file at the top of your index.ts:

import dotenv from "dotenv";
import path from 'path'; 

dotenv.config({ path: path.resolve(__dirname, '../.env') });

// After compiling, index.ts goes to dist/index.js
// So we need to point to the parent directory for .env

4. Setup Database and Build

If you're using Prisma ORM, generate schema and migrations:

npx prisma generate
npx prisma migrate deploy
pnpm run build  # or npm run build

5. Setup PM2 for 24/7 Running

Install PM2 globally to keep your app running:

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

Copy and run the output from pm2 startup:

sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u ubuntu --hp /home/ubuntu

Check if everything is working:

pm2 list
pm2 log your-app-name
sudo reboot  # Test persistence
pm2 list     # Should show your app running

To restart with updated environment variables:

pm2 restart your-app-name --update-env

Your backend is now accessible at <public-ip>:3000.

Hosting on Port 80 (HTTP)

1. Install and Configure Nginx

Install Nginx as a reverse proxy:

sudo apt update && sudo apt install nginx

Configure Nginx:

sudo nano /etc/nginx/nginx.conf

Add this configuration:

events {
  worker_connections 1024;
}

http {
  server {
    listen 80;
    server_name your-domain.com www.your-domain.com;

    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/app_access.log;
    error_log /var/log/nginx/app_error.log warn;
  }
}

2. Test and Enable Nginx

sudo nginx -t
# Expected output: syntax ok, test successful

sudo systemctl reload nginx
sudo systemctl enable nginx  # Auto-start on boot
sudo systemctl status nginx

Your backend is now accessible at <your-domain.com> (port 80).

Securing with HTTPS (Port 443)

1. Setup Domain and DNS

  • Buy a domain (refer to tld-list for best rates)
  • Create an A record in your DNS settings pointing to your EC2 instance's public IPv4 address
  • Wait for DNS propagation (can take some time)

2. Install and Run Certbot

sudo apt update && sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d api.your-domain.com

3. Verify Configuration

Certbot automatically updates your nginx.conf. Verify and reload:

sudo nginx -t
sudo systemctl reload nginx

Your final nginx.conf will include SSL certificates managed by Certbot:

events {
  worker_connections 1024;
}

http {
  server {
    server_name api.your-domain.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_access.log;
    error_log /var/log/nginx/api_error.log warn;
    
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/api.your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.your-domain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
  }

  server {
    if ($host = api.your-domain.com) {
      return 301 https://$host$request_uri;
    }

    listen 80;
    server_name api.your-domain.com;
    return 404;
  }
}

Your backend is now secured with HTTPS at https://api.your-domain.com.

Frontend Deployment

Building the React App

1. Build with Environment Variables

Compile your React app with environment variables:

REACT_APP_VITE_BACKEND_URL=https://api.your-domain.com \
REACT_APP_GEMINI_API=https://api.gemini.com \
REACT_APP_RESEND_API=https://api.resend.com \
pnpm run build

Or using .env.production:

REACT_APP_VITE_BACKEND_URL=https://api.your-domain.com pnpm run build

S3 Bucket Setup

1. Create and Configure S3 Bucket

  • Create a new S3 bucket
  • Upload the contents of the dist folder (not the folder itself)
  • Go to Properties → scroll down → enable Static website hosting and set index.html as the root element

2. Set Permissions and Bucket Policy

  • Go to Permissions → block all public access (CloudFront will handle 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::ACCOUNT-ID/DISTRIBUTION-ID"
        }
      }
    }
  ]
}

CloudFront Distribution Setup

1. Create Distribution

  • Select your S3 bucket as the origin
  • Enable Use Bucket Endpoint
  • Keep default settings or customize as needed

2. Add Origins and Domains

  • Add your EC2 instance as another origin (if serving both static and API routes)
  • Copy the CloudFront domain name to use in your nginx.conf
  • Add your custom domain in Alternate Domain Names (self-explanatory setup)

Your frontend is now deployed and cached globally via CloudFront.

Summary

You've successfully deployed a full-stack MERN application on AWS:

  • Backend: EC2 + Nginx + Let's Encrypt SSL
  • Frontend: S3 + CloudFront CDN
  • Security: HTTPS enabled on both services

Don't worry if it takes a few attempts—there's a lot to configure! Take your time and follow each step carefully.