How to Deploy a MERN Stack Notes App on AWS: Complete Guide
By Braincuber Team
Published on March 2, 2026
We watched a founder burn $2,300 in AWS charges in 11 days because he left 3 RDS instances running on db.m5.large while "testing." He thought the Free Tier covered everything. It doesn't. And that's the kind of mistake you make when you deploy to AWS by copying Stack Overflow commands without understanding what each service actually does. This beginner guide is the complete tutorial for shipping a MERN stack notes app to AWS — EC2 for your Express API, RDS Postgres for the database, S3 for the React frontend — without accidentally lighting your credit card on fire.
What You'll Learn:
- How to verify your MERN app works locally before touching AWS
- Setting up RDS Postgres with Free Tier settings that won't surprise-bill you
- Configuring EC2 security groups so your database isn't exposed to the internet
- Deploying your Express API with PM2 for zero-downtime restarts
- Hosting your React/Vite frontend on S3 with optional CloudFront CDN
- Troubleshooting the 5 most common AWS deployment failures
- Hardening your setup with HTTPS, backups, and cost-saving measures
The AWS Architecture You're Actually Building
AWS throws a wall of jargon at you — VPCs, subnets, security groups, NAT gateways. Ignore 80% of it. Here's what actually happens when a user opens your notes app in the browser. Picture this flow and the AWS console screens stop being scary.
S3 — Your Static Frontend
Browser loads the built React/Vite app (HTML, CSS, JS) directly from an S3 bucket configured for static website hosting. Add CloudFront in front for HTTPS and edge caching — page loads drop from 1.2s to 180ms.
EC2 — Your Express API Server
A t3.micro instance running Amazon Linux 2023 with Node.js 20 and PM2. Your React frontend calls this EC2 box over HTTP/HTTPS for all CRUD operations on notes. Security group allows ports 22, 80, and 443.
RDS Postgres — Your Database
A managed db.t3.micro Postgres instance with 20 GB storage. EC2 talks to RDS on port 5432 inside your VPC only. Public access is disabled. Security group allows connections exclusively from the EC2 security group.
Security Groups — Your Firewall
The invisible rules controlling who talks to what. Allow 80/443 to EC2 from the world. Allow 5432 to RDS only from the EC2 security group. Get this backwards and your Postgres credentials are exposed to every port scanner on the planet.
AWS Free Tier — What It Actually Covers (and What It Doesn't)
The Free Tier is real, but it's not unlimited. Cross these thresholds and AWS starts billing you — silently.
| AWS Service | Free Tier Limit | If You Exceed | Common Trap |
|---|---|---|---|
| EC2 | 750 hrs/month (t2.micro or t3.micro) | ~$8.50/month per instance | Running 2 instances = 1,500 hrs, half billable |
| RDS | 750 hrs/month (db.t3.micro) + 20 GB | ~$14.60/month | Choosing db.t3.small instead of micro |
| S3 | 5 GB storage + 20k GET requests | Pennies for small sites | Uploading large media files without lifecycle rules |
| CloudFront | 1 TB data transfer out | $0.085/GB | Serving uncompressed assets without cache headers |
Free Tier Expires After 12 Months
The AWS Free Tier clock starts the day you create your account, not the day you launch your first resource. If your account is 11 months old and you've never used EC2, you only get 1 month of free hours. Set a Billing Alarm in CloudWatch at $5 so you find out before AWS does.
The 8-Step Deployment Walkthrough
Stop guessing. Follow these steps in order. Skip one and you'll spend 3 hours debugging something that should've taken 5 minutes.
Run the App Locally First
Before touching AWS, prove the app works on your machine. Clone the repo, run npm install in both /backend and /frontend, copy .env.example to .env, set DATABASE_URL to a local Postgres instance, and run npm run dev in both directories. Open http://localhost:5173, create a note, refresh. If it persists, you're good. If it doesn't, fix it here — not on EC2 where every debug cycle takes 4x longer.
Push Your Code to GitHub
Your EC2 instance needs somewhere to pull code from. Run git init, add your remote, and push to main. Critical: add .env to your .gitignore before committing. We've seen founders push database passwords to public repos. GitHub bots scrape for these within seconds. Your RDS instance gets crypto-mined overnight. Not theoretical — we've cleaned up 3 of these in the last year.
Create Your RDS Postgres Instance
Go to RDS > Create database > PostgreSQL > Free Tier template. Set class to db.t3.micro, storage to 20 GB gp3. Set your master username and password — you'll need these for DATABASE_URL. Set Public access: No. Create a security group that allows port 5432 only from the EC2 security group (not from 0.0.0.0/0). Enable automated backups and Require SSL. Save the RDS endpoint — it looks like mydb.abc123.us-east-1.rds.amazonaws.com.
Create Your S3 Bucket for the Frontend
Go to S3 > Create bucket. Name it something unique like mern-notes-frontend-yourname. For simple hosting: enable Static website hosting, set index document to index.html, and allow public reads via a bucket policy. For production: keep the bucket private and put CloudFront + OAC in front of it instead. Turn on versioning if you want the ability to roll back a broken deploy in 30 seconds.
Launch and Configure Your EC2 Instance
Launch Amazon Linux 2023, size t3.micro. Security group inbound rules: 22 (SSH — your IP only), 80 (HTTP — world), 443 (HTTPS — if applicable). SSH into the box and install: sudo dnf update -y, sudo dnf install -y git, add NodeSource repo for Node.js 20, install PM2 globally. Clone your repo, run npm install in the backend directory, create your .env file with PORT=80, DATABASE_URL pointing to your RDS endpoint, DATABASE_SSL=true, and CORS_ORIGIN matching your frontend URL.
Start the API with PM2 and Verify
Run pm2 start server.js --name mern-notes-api, then pm2 save and pm2 startup systemd so it survives reboots. Test on the box with curl http://localhost/api/health — you should get {"status":"ok"}. From your laptop, hit http://<ec2-public-dns>/api/health. If you get a timeout, your security group isn't allowing port 80. If you get a 502, PM2 crashed — check pm2 logs mern-notes-api for a bad DATABASE_URL or missing SSL flag.
Build and Upload the React Frontend to S3
On your local machine, set VITE_API_URL to your EC2 public DNS (e.g., http://ec2-xx-xx.compute.amazonaws.com/api), then run npm run build in the frontend directory. Upload the dist/ folder to S3 with aws s3 sync dist/ s3://your-bucket-name/ --delete. The --delete flag removes old files that no longer exist. Open your S3 website URL or CloudFront distribution URL. You should see your notes app.
Secure, Verify End-to-End, and Save Money
Load the frontend, create a note, delete a note, refresh — everything should persist in RDS. Then harden: disable SSH after setup (or switch to SSM Session Manager), add HTTPS via CloudFront + ACM or an ALB, keep RDS private, ship PM2 logs to CloudWatch with alarms on CPU and status checks, and snapshot RDS daily. Stop EC2 when you're not using it — every hour it runs counts against your 750-hour Free Tier allowance.
The Environment Variables That Break 90% of Deployments
Misconfigured env vars are the #1 reason MERN deployments fail on AWS. Not architecture. Not code. A missing DATABASE_SSL=true flag or a mistyped CORS_ORIGIN and you'll spend hours debugging something that takes 10 seconds to fix.
PORT=80
DATABASE_URL=postgres://admin:YourPass@mydb.abc123.us-east-1.rds.amazonaws.com:5432/notesdb
DATABASE_SSL=true
CORS_ORIGIN=https://your-frontend.cloudfront.net
VITE_API_URL=http://ec2-xx-xx-xx-xx.compute.amazonaws.com/api
The 5 Failures You'll Hit (and How to Fix Each One)
Every single one of these has burned at least 2 hours of our time on client deployments. Bookmark this section.
| Symptom | Cause | Fix |
|---|---|---|
| API returns 500 | Bad DATABASE_URL or missing DATABASE_SSL=true | Run pm2 logs mern-notes-api — the error message is right there |
| DB connection timeout | RDS security group doesn't allow EC2 SG on port 5432 | Edit RDS SG inbound — source must be the EC2 SG ID, not an IP |
| CORS errors in browser | CORS_ORIGIN doesn't match frontend origin exactly | Include protocol and no trailing slash: https://d1abc.cloudfront.net |
| 403 from S3 | Bucket policy missing or static hosting not enabled | Enable static website hosting + add public read bucket policy, or use CloudFront OAC |
| Blank page on S3 URL | dist/ uploaded to wrong bucket or index.html not set | Check S3 bucket contents — you need index.html at the root, not inside a dist/ subfolder |
The CORS_ORIGIN Gotcha
CORS_ORIGIN must match your frontend origin exactly — protocol, domain, port, no trailing slash. http:// is not the same as https://. www.example.com is not the same as example.com. We've burned 4.5 hours debugging a "CORS error" that was a missing s in https.
Frequently Asked Questions
How much does it cost to deploy a MERN stack app on AWS?
Under the Free Tier (first 12 months), a t3.micro EC2 instance, db.t3.micro RDS Postgres, and S3 static hosting costs $0 if you stay within limits. After Free Tier expires, expect roughly $23–$35/month for this exact setup.
Why use PM2 instead of just running node server.js on EC2?
PM2 automatically restarts your API if it crashes and survives server reboots via the startup command. Running node directly means one unhandled exception kills your API until you manually SSH in and restart it.
Can I use MongoDB instead of RDS Postgres for this deployment?
Yes. Swap RDS for MongoDB Atlas (free M0 tier) and update DATABASE_URL to your Atlas connection string. The deployment steps for EC2, S3, and CloudFront remain identical — only the database layer changes.
Do I need CloudFront for a MERN app deployed on AWS?
Not strictly. S3 static website hosting works without CloudFront. But CloudFront gives you HTTPS, edge caching (faster loads globally), and lets you keep the S3 bucket private via OAC. For production apps, it's worth the extra 10 minutes of setup.
What's the next step after manually deploying to AWS?
Automate it. Use Terraform or AWS CDK to recreate the entire stack with one command. Add GitHub Actions for CI/CD so every push to main auto-deploys. Consider containerizing with Docker and moving to ECS Fargate for easier scaling.
Stuck Deploying Your App to AWS?
We've deployed 47 production apps to AWS across EC2, ECS, Lambda, and S3 — for D2C brands doing $1M to $10M. We'll architect your infrastructure, set up CI/CD, configure security groups, and make sure your Free Tier isn't bleeding money. Stop guessing at AWS console buttons. Talk to someone who's done it.
