How to Deploy a MERN Application Using AWS S3, CloudFront and EC2
Learn how to deploy a MERN Stack application on AWS using S3, CloudFront, and EC2. Configure React hosting, Node.js, Express, Nginx, HTTPS, MongoDB Atlas, and CI/CD.
Introduction
Building a MERN Stack application is only one part of the development process.
Once your React frontend, Node.js backend, Express APIs, and MongoDB database are ready, the next challenge is deploying everything into a production environment.
You need somewhere to host your frontend, somewhere to run your backend, a way to deliver static assets quickly, a secure connection between services, and a reliable database connection.
This is where AWS services such as Amazon S3, CloudFront, and EC2 become useful.
Instead of hosting the entire application on one server, we can separate the frontend and backend responsibilities:
Amazon S3: Stores the production React build.
Amazon CloudFront: Delivers frontend assets through a global CDN.
Amazon EC2: Runs the Node.js and Express backend.
MongoDB Atlas: Stores application data.
Nginx: Acts as a reverse proxy for the backend API.
In this guide, we'll understand the complete architecture and deploy a MERN application step by step.
By the end, you'll understand how a real production request travels from the browser to AWS infrastructure and eventually reaches your backend and database.
1. Understanding the AWS Deployment Architecture
Before deploying anything, let's understand how the services communicate.
Imagine a MERN application with the following structure:
USER
|
v
example.com
|
v
CLOUDFRONT
(AWS CDN)
|
+--------+--------+
| |
v v
S3 ORIGIN EC2 ORIGIN
React Frontend Backend API
| |
v v
Static Assets Nginx
|
v
Node.js + Express
|
v
MongoDB AtlasThis is a logical architecture, not a requirement to send every request through S3 and then EC2.
CloudFront can route requests to different origins based on URL path patterns.
For example:
Request pathOriginPurpose/S3React application/assets/*S3JavaScript, CSS, images/api/*EC2Backend API/api/auth/*EC2Authentication endpoints
CloudFront acts as the entry point and decides which origin should receive each request.
Why separate the frontend and backend?
There are several advantages:
Independent deployments: You can update your React application without restarting your Node.js backend.
Better static asset delivery: CloudFront caches frontend assets at edge locations closer to users.
Resource separation: Frontend traffic and backend processing are handled by different services.
Flexible scaling: You can optimize frontend delivery and backend compute resources independently.
Improved security: S3 can remain private while CloudFront provides controlled access to frontend files.
2. Preparing the MERN Application
Let's assume our project has this structure:
mern-production-app/
│
├── client/
│ ├── src/
│ ├── public/
│ ├── package.json
│ └── vite.config.js
│
└── server/
├── src/
├── package.json
├── index.js
└── .envThe client directory contains the React frontend.
The server directory contains the Node.js and Express backend.
First, make sure your backend uses an environment variable for its port and database connection.
Example:
// server/index.js
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
dotenv.config();
const app = express();
app.use(express.json());
app.use(cors({
origin: process.env.CLIENT_URL,
}));
app.get("/api/health", (req, res) => {
res.json({
status: "healthy",
message: "Backend is running",
});
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, "127.0.0.1", () => {
console.log(`Server running on port ${PORT}`);
});Your production environment file might contain:
PORT=5000
MONGODB_URI=mongodb+srv://...
CLIENT_URL=https://example.com
NODE_ENV=productionDo not commit this file to Git or expose database credentials in frontend code.
For the React application, configure the API base URL.
For example:
VITE_API_URL=https://example.com/apiThen use it in your frontend:
const API_URL = import.meta.env.VITE_API_URL;
export async function getUsers() {
const response = await fetch(`${API_URL}/users`);
if (!response.ok) {
throw new Error("Unable to fetch users");
}
return response.json();
}Remember that Vite environment variables prefixed with VITE_ are included in the client bundle. Never put secrets in them.
3. Creating an AWS S3 Bucket for React
Amazon S3 is an object storage service.
It stores files such as images, documents, JavaScript bundles, CSS files, and other static assets.
For our MERN application, we'll use S3 to store the production React build.
Step 1: Create an S3 bucket
Open the AWS Management Console.
Navigate to:
S3 → Buckets → Create bucket
Choose a unique bucket name, for example:
asad-mern-production-frontendSelect your preferred AWS Region.
For a CloudFront-based private-origin deployment, keep Block Public Access enabled.
We'll configure CloudFront to access the bucket using Origin Access Control (OAC).
Step 2: Build the React application
Navigate to your frontend directory:
cd clientInstall dependencies:
npm installCreate the production build:
npm run buildFor a Vite application, this normally generates:
client/
└── dist/
├── index.html
├── assets/
│ ├── index-abc123.js
│ └── index-def456.css
└── favicon.icoThe dist directory contains the optimized production files.
Step 3: Upload the build to S3
You can upload the build through the AWS Console or AWS CLI.
First, configure AWS CLI credentials securely using an IAM identity or role.
Then run:
aws s3 sync dist/ \
s3://asad-mern-production-frontend \
--deleteWhat does this command do?
aws s3 sync: Synchronizes local files with the S3 bucket.dist/: Specifies the production build directory.s3://...: Specifies the destination bucket.--delete: Removes destination objects that no longer exist locally.
Be careful with --delete: it can remove files from your bucket. Use it only when the target bucket is dedicated to the frontend build.
Your React application is now stored in S3.
However, users should not need to access the bucket directly.
That's where CloudFront comes in.
4. Configuring CloudFront as a CDN
Amazon CloudFront is a Content Delivery Network (CDN).
It distributes content through a global network of edge locations.
When a user requests a frontend asset, CloudFront can serve a cached copy from an edge location instead of fetching it from the S3 origin every time.
For example:
User in Pakistan
|
v
CloudFront Edge
|
Cache Available?
/ \
Yes No
| |
v v
Return File Fetch from S3
| |
| Cache File
| |
+-----+----+
|
v
Return ResponseThis reduces repeated origin requests and can improve content delivery latency.
Step 1: Create a CloudFront distribution
Navigate to:
CloudFront → Distributions → Create distribution
Choose your S3 bucket as the origin.
For security, configure Origin Access Control (OAC).
OAC allows CloudFront to access a private S3 bucket without making the bucket publicly accessible.
Step 2: Configure S3 bucket permissions
CloudFront needs permission to read the frontend objects.
Use the bucket policy generated for your distribution or configure a policy that grants s3:GetObject to the specific CloudFront distribution.
Conceptually:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontReadOnly",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::asad-mern-production-frontend/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
}
}
]
}Replace the account ID and distribution ID with your actual values.
Step 3: Configure the default root object
Set:
index.htmlThis tells CloudFront which file to return when users visit the root URL.
Step 4: Configure SPA routing
React applications often use client-side routing.
For example:
/dashboard
/profile
/settingsThese routes may not correspond to actual files inside S3.
If a user refreshes /dashboard, the origin may return a 403 or 404 because no physical /dashboard object exists.
For a client-side React application, configure CloudFront to serve index.html for appropriate frontend route errors.
Be careful not to rewrite API errors to index.html.
A better approach is to separate frontend and API behaviors so that only frontend navigation requests use the SPA fallback.
5. Deploying the Node.js Backend on EC2
Now let's deploy the Express backend.
Amazon EC2 provides virtual servers in the AWS cloud.
Unlike Lambda, an EC2 instance runs your application continuously until you stop or terminate the instance.
This makes EC2 useful for applications requiring persistent processes, long-running tasks, or server-level configuration.
Step 1: Launch an EC2 instance
Navigate to:
EC2 → Instances → Launch instance
Choose:
Instance name:
mern-production-apiOperating system: Ubuntu Server LTS
Instance type: Select according to workload and budget
Key pair: Create or select an SSH key pair
Storage: Configure according to application requirements
For a small application, you can begin with a modest instance and monitor resource utilization.
Step 2: Configure security groups
Security groups act as virtual firewalls for EC2 instances.
For our setup, the inbound rules might look like this:
PortSourcePurpose22Your trusted IPSSH administration80Appropriate network sourceHTTP, if Nginx serves it443Appropriate network sourceHTTPS, if TLS terminates on EC25000No public accessInternal Node.js application
If CloudFront is the public entry point and Nginx receives API traffic, do not expose port 5000 publicly.
For stronger isolation, consider an Application Load Balancer or a controlled CloudFront-to-backend origin design.
Step 3: Connect to EC2
Use SSH:
ssh -i your-key.pem ubuntu@YOUR_EC2_PUBLIC_IPUpdate system packages:
sudo apt update
sudo apt upgrade -yInstall Node.js using a trusted installation method appropriate for your selected version.
Verify the installation:
node -v
npm -vStep 4: Deploy backend code
Clone your repository:
git clone https://github.com/your-username/mern-production-app.gitNavigate to the backend:
cd mern-production-app/serverInstall production dependencies:
npm ci --omit=devConfigure your production environment variables securely.
For example:
NODE_ENV=production
PORT=5000
MONGODB_URI=your-production-mongodb-uri
CLIENT_URL=https://example.comIn a real production environment, use a managed secrets solution such as AWS Systems Manager Parameter Store or AWS Secrets Manager rather than keeping sensitive credentials in a repository.
Step 5: Run the application with PM2
PM2 is a process manager for Node.js applications.
It can keep your application running, restart it after crashes, and provide process logs.
Install PM2:
sudo npm install -g pm2Start your backend:
pm2 start index.js --name mern-apiCheck running processes:
pm2 listView logs:
pm2 logs mern-apiSave the process list:
pm2 saveConfigure PM2 startup:
pm2 startupRun the command PM2 prints to enable startup on system boot.
Now your Node.js application can run as a managed background process.
6. Configuring Nginx as a Reverse Proxy
Your Express application is listening on:
127.0.0.1:5000But users should not access that internal port directly.
Nginx can receive HTTP requests and forward them to the Node.js application.
This is called reverse proxying.
Install Nginx:
sudo apt install nginx -yCreate a configuration file:
sudo nano /etc/nginx/sites-available/mern-apiAdd:
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Enable the configuration:
sudo ln -s \
/etc/nginx/sites-available/mern-api \
/etc/nginx/sites-enabled/Test the configuration:
sudo nginx -tReload Nginx:
sudo systemctl reload nginxNow Nginx forwards incoming requests to Express.
For a production HTTPS setup, terminate TLS at CloudFront or an Application Load Balancer, or configure TLS directly on Nginx. Ensure the origin connection is encrypted and that the origin cannot be accessed through an unintended public route.
7. Connecting CloudFront to Both S3 and EC2
This is where the complete architecture comes together.
CloudFront supports multiple origins and cache behaviors.
We can configure:
Origin 1: S3
asad-mern-production-frontendOrigin 2: EC2 backend
api.example.comThen configure path-based behaviors.
Path patternOriginHTTP methodsDefault *S3GET, HEAD, OPTIONS/api/*EC2Methods required by your API
For the API behavior:
Forward the required authorization headers.
Forward query strings.
Forward cookies if your authentication uses them.
Configure an appropriate cache policy.
Disable caching for sensitive dynamic API responses unless explicitly designed otherwise.
Allow the HTTP methods your backend needs.
For authentication endpoints and user-specific responses, caching should generally be disabled or carefully configured to prevent one user's response from being served to another user.
The request flow now looks like:
Browser
|
v
CloudFront
|
+---------------------+
| |
v v
Default Behavior /api/*
| |
v v
Private S3 Origin EC2 / Nginx
| |
v v
React Build Express API
|
v
MongoDB AtlasThis separation is one of the most useful features of this architecture.
The frontend and backend share one public entry point while remaining independently hosted.
8. Configuring HTTPS and a Custom Domain
A production application should use HTTPS.
AWS Certificate Manager (ACM) can provide SSL/TLS certificates for supported AWS integrations.
For a custom domain such as:
example.comand:
www.example.comyou can configure:
A custom domain in CloudFront.
A validated ACM certificate in the
us-east-1Region for CloudFront.DNS records pointing the domain to the CloudFront distribution.
Appropriate origin security and HTTPS settings.
If you use an Application Load Balancer for your EC2 backend, its ACM certificate must be in the same Region as the load balancer.
For a direct EC2 origin, you can configure HTTPS at Nginx using an appropriate certificate.
Do not assume that HTTPS between the browser and CloudFront automatically encrypts the connection between CloudFront and your origin. Configure the origin protocol policy correctly.
9. Connecting MongoDB Atlas
Our Express application now runs on EC2.
The next step is connecting it to MongoDB Atlas.
MongoDB Atlas is a managed MongoDB database service.
Instead of installing and managing MongoDB directly on your EC2 instance, Atlas handles much of the database infrastructure.
Step 1: Create a MongoDB Atlas cluster
Create a cluster and configure your database user.
Step 2: Configure network access
Allow your EC2 instance to connect to Atlas.
For production, prefer private connectivity where supported or an appropriate stable outbound IP with a narrowly scoped network access rule.
Avoid allowing access from 0.0.0.0/0 simply to make the connection work.
Step 3: Configure the connection string
Add the connection string to your backend's secret configuration:
MONGODB_URI=mongodb+srv://...Then connect through your Node.js application:
import mongoose from "mongoose";
export async function connectDatabase() {
try {
await mongoose.connect(process.env.MONGODB_URI);
console.log("MongoDB connected successfully");
} catch (error) {
console.error("MongoDB connection failed:", error);
process.exit(1);
}
}Call connectDatabase() during application startup before accepting requests.
Now the backend can read and write application data through MongoDB Atlas.
10. What Happens When You Deploy a New Version?
Imagine you update your React dashboard and fix a backend API issue.
How do you deploy the changes?
The frontend and backend can be deployed independently.
Frontend deployment
Build the updated React application:
cd client
npm run buildUpload the new build:
aws s3 sync dist/ \
s3://asad-mern-production-frontend \
--deleteThen invalidate the relevant CloudFront cache:
aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/*"In production, use content-hashed asset filenames and long-lived caching for immutable assets. Keep index.html on a short or no-cache policy so users receive the latest application entry point.
Backend deployment
Connect to EC2 and update your code:
cd mern-production-app/server
git pull origin main
npm ci --omit=devRestart the application:
pm2 restart mern-apiFor production deployments, use a CI/CD pipeline and a deployment strategy that keeps the API available during updates. A rolling or blue-green deployment with multiple instances is more reliable than restarting the only running process.
11. Automating Deployment with CI/CD
Manually deploying every update becomes difficult as an application grows.
You can automate deployment using GitHub Actions.
A typical pipeline looks like:
Developer Pushes Code
|
v
GitHub Repository
|
v
GitHub Actions
|
v
Install Packages
|
v
Run Tests
|
v
Build React
|
v
Upload to S3
|
v
Invalidate CloudFront
|
v
Deploy Backend
|
v
Verify DeploymentA frontend workflow can build the React application and upload its output to S3.
For authentication, use GitHub Actions OIDC with an appropriately restricted AWS IAM role instead of long-lived AWS access keys.
For backend deployment, you can use AWS Systems Manager, a controlled deployment runner, or another secure deployment mechanism.
The goal is to make deployments repeatable, predictable, and less dependent on manual server access.
12. Common Production Problems
Deploying an application successfully does not mean everything will work perfectly.
Here are some issues developers commonly encounter.
Problem 1: React routes return 403 or 404 after refresh
Cause: S3 does not contain a physical file for every client-side route.
Solution: Configure CloudFront SPA routing fallback for frontend navigation while keeping API errors separate.
Problem 2: API requests fail because of CORS
Cause: The backend does not allow the frontend's actual origin, or browser requests use an incorrect origin or credentials configuration.
Solution: Configure Express CORS for the correct frontend domain and authentication method.
Problem 3: CloudFront keeps serving an old frontend build
Cause: Cached objects have not expired or been invalidated.
Solution: Use content-hashed asset filenames, appropriate cache headers, and targeted invalidations when needed.
Problem 4: EC2 backend stops responding
Cause: The process crashed, the instance ran out of resources, or networking and security rules are incorrect.
Solution: Monitor application logs, EC2 metrics, process health, and infrastructure alerts. Use an appropriate process manager and recovery strategy.
Problem 5: MongoDB connection fails
Cause: Incorrect credentials, network access restrictions, DNS issues, or connection limits.
Solution: Verify the connection string, Atlas network access, database user permissions, and outbound connectivity.
Problem 6: API requests receive 502 or 504 errors
Cause: Nginx cannot reach the backend, or the upstream service takes too long to respond.
Solution: Check PM2 logs, Nginx logs, backend health endpoints, and timeout settings.
13. Security and Production Best Practices
Before making your application publicly available, consider these practices:
AWS security
Use IAM roles instead of hardcoded AWS credentials.
Keep the S3 bucket private with CloudFront OAC.
Restrict EC2 security group rules.
Avoid exposing the Node.js application port publicly.
Store secrets in AWS Secrets Manager or Systems Manager Parameter Store.
Application security
Use HTTPS.
Validate incoming requests.
Configure CORS carefully.
Apply authentication and authorization on backend routes.
Never expose database credentials in frontend bundles.
Performance
Use CloudFront caching for static assets.
Enable compression where appropriate.
Use content-hashed filenames.
Optimize React bundle size.
Monitor EC2 CPU, memory, disk, and network usage.
Reliability
Configure application health checks.
Monitor CloudFront, EC2, and application logs.
Back up important database data.
Establish deployment rollback procedures.
Consider an Application Load Balancer and multiple EC2 instances when higher availability is required.
14. Is This Architecture Suitable for Every MERN Application?
Not necessarily.
S3, CloudFront, and EC2 provide a practical architecture for applications that need separately hosted frontend and backend services.
However, the right architecture depends on traffic, reliability requirements, operational complexity, and budget.
For example:
RequirementPossible AWS solutionStatic React hostingS3 + CloudFrontNode.js backendEC2Managed container deploymentECS + FargateServerless API operationsLambda + API GatewayManaged relational databaseRDSObject storageS3Global content deliveryCloudFrontSecrets managementSecrets ManagerMonitoringCloudWatch
For a small or medium-sized application, a single EC2 instance may be a reasonable starting point.
For a high-availability production system, consider multiple EC2 instances behind an Application Load Balancer, automated scaling, deployment automation, and stronger monitoring.
The objective is not to use every AWS service.
It is to choose the services that solve your actual application requirements.
Final Thoughts
Deploying a MERN application on AWS is not simply about uploading frontend files and starting a Node.js server.
It involves understanding how different infrastructure components communicate and how each service contributes to the complete application.
Amazon S3 stores and serves the frontend assets. CloudFront delivers those assets globally and routes requests to the appropriate origin. EC2 runs the backend application, Nginx forwards API requests, and MongoDB Atlas stores application data.
Once these components are configured correctly, you have a practical production architecture with independent frontend and backend deployments.
As full-stack developers, understanding deployment architecture is just as important as writing application code.
A production-ready application is not only one that works locally. It is one that can be deployed, secured, monitored, maintained, and scaled reliably.
About the Author
Asad Saeed | Senior Software Engineer | Full Stack Developer
I share practical insights on backend engineering, MERN Stack development, distributed systems, database architecture, cloud computing, and building reliable production applications.




