A complete step-by-step guide to deploying a full-stack MERN (MongoDB Atlas, Express, React, Node.js) application on an AWS EC2 instance with Nginx as a reverse proxy and PM2 as a process manager.
- Tech Stack
- Prerequisites
- EC2 Instance Setup
- Clone Your Project
- Backend Setup
- Run Backend with PM2
- Frontend Build
- Install & Configure Nginx
- Verify Deployment
- Accessing Your App
- Updating Your App Remotely
- Common Mistakes
- Quick Reference
- Project Structure
| Layer | Technology |
|---|---|
| Frontend | React.js |
| Backend | Node.js + Express.js |
| Database | MongoDB Atlas (Cloud) |
| Process Manager | PM2 |
| Web Server / Proxy | Nginx |
| Cloud Platform | AWS EC2 (Ubuntu / Amazon Linux 2) |
| Auth | JWT (JSON Web Tokens) |
Before you begin, make sure you have:
- An AWS account with EC2 access
- A MongoDB Atlas account with a cluster created
- Your project pushed to a GitHub repository
- A
.pemkey pair file downloaded from AWS - EC2 Security Group configured with these inbound rules:
| Type | Protocol | Port | Source |
|---|---|---|---|
| SSH | TCP | 22 | Your IP |
| HTTP | TCP | 80 | 0.0.0.0/0 |
| Custom TCP | TCP | 5000 | 0.0.0.0/0 |
1. Go to https://cloud.mongodb.com β Create free account
2. Create a FREE cluster (M0 Sandbox)
3. Database Access β Add DB User (username + strong password)
4. Network Access β Add IP Address β Allow from Anywhere (0.0.0.0/0)
5. Connect β Drivers β Copy your connection string
# Your connection string will look like:
mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/<dbname>?retryWrites=true&w=majority# SSH into your EC2 instance
ssh -i your-key.pem ubuntu@<YOUR-EC2-PUBLIC-IP>
# Switch to superuser
sudo su
# Update system packages
apt update -y # Ubuntu / Debian
# OR
yum update -y # Amazon Linux
# Verify your current directory and user
pwd
whoami
# Install Node.js 18
curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -
sudo apt install -y nodejs # Ubuntu
# OR
sudo yum install -y nodejs # Amazon Linux
# Install Git
sudo apt install -y git # Ubuntu
# OR
sudo yum install -y git # Amazon Linux
# Verify installations
node -v
npm -v
git --version# Navigate to home directory
cd ~
# Clone your GitHub repository
git clone https://github.com/<your-username>/<your-repo>.git
# Verify it cloned successfully
ls
# Enter the project folder
cd <your-repo>/
ls# Navigate to backend folder
cd <your-backend-folder>/
# Check folder contents
ls
# Install backend dependencies
npm install
# Set up environment variables
cp .env.example .env # Copy the example file
nano .env # Edit and fill in your actual valuesYour .env file should contain:
PORT=5000
MONGODB_URI=mongodb+srv://<user>:<pass>@cluster0.xxxxx.mongodb.net/ecommercedb
JWT_SECRET=your_super_secret_jwt_key_here
NODE_ENV=production# (Optional) Import seed / sample data
npm run data:import
# Test that the backend starts correctly
npm start
# β
You should see: "Server running on port 5000"
# β
You should see: "MongoDB Atlas Connected"
# Press Ctrl+C to stop β we'll use PM2 next# Install PM2 globally
sudo npm install -g pm2
# Navigate to folder containing server.js
cd <your-backend-folder>/
# Start the backend with PM2
pm2 start server.js --name backend
# Save the PM2 process list
pm2 save
# Generate PM2 startup script (auto-restart on server reboot)
pm2 startup
# β οΈ PM2 will print a command β copy and run it. It looks like:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 \
startup systemd -u ubuntu --hp /home/ubuntu
# Save again after running the startup command
pm2 save
# Verify the backend is running
pm2 list
pm2 logsExpected pm2 list output:
βββββββ¬βββββββββββ¬ββββββββββ¬ββββββββ¬βββββββββββ¬βββββββ
β id β name β mode β βΊ β status β cpu β
βββββββΌβββββββββββΌββββββββββΌββββββββΌβββββββββββΌβββββββ€
β 0 β backend β fork β 0 β online β 0% β
βββββββ΄βββββββββββ΄ββββββββββ΄ββββββββ΄βββββββββββ΄βββββββ
# Navigate to frontend folder
cd ~/<your-repo>/<your-frontend-folder>/
# Install frontend dependencies
npm install
# Build for production
npm run build
# Verify build output folder was created
ls
# β
You should see a /build or /dist folderNote: React apps output to
/build, while Vite apps output to/dist. Check which one your project uses.
# Ubuntu / Debian
sudo apt install nginx -y
# Amazon Linux
sudo yum install nginx -y
# Start and enable Nginx
sudo systemctl start nginx
sudo systemctl enable nginx # Auto-start on every reboot
# Check Nginx is active
sudo systemctl status nginx# Open the default config file
sudo nano /etc/nginx/sites-available/defaultReplace the contents with:
server {
listen 80;
server_name <YOUR-EC2-PUBLIC-IP>;
# β
Serve React frontend
# Use /build for Create React App or /dist for Vite
root /home/ubuntu/<your-repo>/<your-frontend-folder>/dist;
index index.html;
# β
Handle React Router (SPA fallback)
location / {
try_files $uri $uri/ /index.html;
}
# β
Proxy all /api requests to Express backend
location /api/ {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $http_upgrade;
}
}# Save and exit: Ctrl+X β Y β Enter
# β
Test Nginx configuration for syntax errors
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
# Restart Nginx to apply the new configuration
sudo systemctl restart nginx# β
Check PM2 backend status
pm2 list
pm2 logs
# β
Check Nginx status
sudo systemctl status nginx
# β
Check Nginx error logs (if something looks wrong)
sudo tail -f /var/log/nginx/error.log
# β
Check Nginx access logs
sudo tail -f /var/log/nginx/access.logOnce everything is running, open a browser and visit:
http://<YOUR-EC2-PUBLIC-IP> β React Frontend
http://<YOUR-EC2-PUBLIC-IP>/api/ β Express Backend API
π‘ To use a domain name instead of an IP, point your domain's DNS A record to your EC2 Public IP and update
server_namein your Nginx config.
When you push new code changes, follow these steps to redeploy:
# 1. SSH into your EC2 instance
ssh -i your-key.pem ubuntu@<EC2-IP>
# 2. Navigate to project folder
cd ~/<your-repo>/
# 3. Pull the latest code
git pull origin main
# 4. Update backend dependencies (if package.json changed)
cd <your-backend-folder>/
npm install
# 5. Restart backend with PM2
pm2 restart backend
# 6. Rebuild the React frontend
cd ~/<your-repo>/<your-frontend-folder>/
npm install
npm run build
# 7. Restart Nginx
sudo systemctl restart nginx
# 8. Confirm everything is live
pm2 list
sudo systemctl status nginx| β Mistake | β Fix |
|---|---|
Forgetting to fill in .env |
Always run cp .env.example .env and edit every value |
| Wrong build folder path in Nginx | Check if your output folder is /build (CRA) or /dist (Vite) |
| Port 5000 not accessible | Add Custom TCP 5000 inbound rule in EC2 Security Group |
| PM2 not surviving reboot | Always run pm2 startup + pm2 save after starting |
| Nginx config syntax error | Always run sudo nginx -t before restarting Nginx |
Running npm start directly |
Use PM2 β npm start blocks terminal and stops on exit |
| MongoDB IP not whitelisted | In Atlas: Network Access β Add 0.0.0.0/0 |
| Frontend calling wrong API URL | Set REACT_APP_API_URL in frontend .env correctly |
| Cloning into wrong directory | Always clone into home directory ~ then cd into it |
| Task | Command |
|---|---|
| SSH into EC2 | ssh -i key.pem ubuntu@<EC2-IP> |
| Check logged-in user | whoami |
| Update system | apt update -y |
| Clone repository | git clone <github-url> |
| Pull latest code | git pull origin main |
| Install packages | npm install |
| Seed database | npm run data:import |
| Build frontend | npm run build |
| Start app with PM2 | pm2 start server.js --name backend |
| Restart PM2 app | pm2 restart backend |
| View PM2 process list | pm2 list |
| View PM2 logs | pm2 logs |
| Stop PM2 app | pm2 stop backend |
| Set PM2 auto-start | pm2 startup then pm2 save |
| Test Nginx config | sudo nginx -t |
| Restart Nginx | sudo systemctl restart nginx |
| Check Nginx status | sudo systemctl status nginx |
| View Nginx error log | sudo tail -f /var/log/nginx/error.log |
| Edit Nginx config | sudo nano /etc/nginx/sites-available/default |
your-repo/
βββ your-backend-folder/
β βββ server.js β PM2 starts this
β βββ package.json
β βββ .env β Fill this in on EC2
β βββ .env.example β Template for .env
β βββ config/
β β βββ db.js β MongoDB Atlas connection
β βββ models/
β βββ routes/
β βββ middleware/
β βββ seed.js / data/ β Sample data importer
β
βββ your-frontend-folder/
βββ package.json
βββ src/
βββ dist/ or build/ β Nginx serves this folder
Admin: admin@shop.com / Admin@1234
User: user@shop.com / User@1234
β οΈ Change these immediately after your first login in production!
Backend not starting?
pm2 logs # Check for errors
node server.js # Run directly to see full error outputFrontend showing blank page?
# Check Nginx root path is correct
sudo nano /etc/nginx/sites-available/default
# Verify the /dist or /build folder actually exists
ls ~/<your-repo>/<your-frontend-folder>/MongoDB connection refused?
# Check .env has correct MONGODB_URI
cat .env
# Check MongoDB Atlas Network Access allows 0.0.0.0/0Nginx showing 502 Bad Gateway?
# Backend is not running β check PM2
pm2 list
pm2 restart backendDeployed on AWS EC2 using MongoDB Atlas, PM2, and Nginx.
This project is licensed under the MIT License.