Recently, I completed a production-style AWS deployment project where I built and deployed a full-stack Application using a proper 3-tier cloud architecture.
The main goal was not only to make the application work, but also to design it in a way that follows real production concepts such as:
- HTTPS
- Custom domain
- Public and private subnets
- Load balancing
- Auto Scaling Groups
- Private database access
- Nginx reverse proxy
- PM2 process management
- GitHub Actions CI/CD
GitHub Repository:
Project Overview
This is a full-stack Application with:
User authentication using JWT
Blog create, read, update, and delete features
Category management
Comment system
Admin and author roles
Protected API routes
React + Vite frontend
Node.js + Express backend
MySQL database hosted on Amazon RDS
The application was first deployed manually on AWS using a complete 3-tier architecture. After the manual deployment was completed and verified, I added GitHub Actions CI/CD pipelines to automate both frontend and backend deployments.
Architecture Diagram
The application follows this flow:
User Browser
↓
Route 53 Custom Domain
↓
ACM SSL Certificate
↓
Public Frontend Application Load Balancer
↓
Frontend Target Group
↓
Frontend Auto Scaling Group
↓
EC2 Instances running Nginx
↓
Nginx serves React build and reverse proxies /api
↓
Internal Backend Application Load Balancer
↓
Backend Target Group
↓
Backend Auto Scaling Group
↓
EC2 Instances running Node.js with PM2
↓
Private RDS MySQL Database
The most important design decision in this project was this:
The browser should not directly access the private backend.
Instead, the browser calls the public frontend domain, and Nginx forwards /api requests to the internal backend load balancer.
Tech Stack
Frontend
- React
- Vite
- JavaScript
- Axios
- Nginx for production serving
Backend
- Node.js
- Express.js
- JWT authentication
- REST API
- PM2 process manager
Database
- MySQL
- Amazon RDS MySQL
AWS Services
- VPC
- Public subnets
- Private application subnets
- Private database subnets
- Internet Gateway
- NAT Gateway
- Route Tables
- EC2
- Application Load Balancer
- Auto Scaling Group
- Launch Template
- Amazon RDS MySQL
- Route 53
- AWS Certificate Manager
- Security Groups
DevOps and CI/CD
- Git
- GitHub
- GitHub Actions
- SSH deployment
- SCP
- Nginx reverse proxy
- PM2
- Linux
Why I Used a 3-Tier Architecture
In a simple deployment, we can run the frontend, backend, and database on one server. That is useful for learning, but it is not suitable for production-style architecture.
In this project, I separated the application into three layers:
1. Frontend Layer
The frontend is a React application built using Vite. In production, the React build files are served by Nginx on EC2 instances.
The frontend layer is public because users need to access the website through the browser.
2. Backend Layer
The backend is a Node.js and Express API. It runs on private EC2 instances and is managed using PM2.
The backend is not directly exposed to the internet. It is accessed through an internal Application Load Balancer.
3. Database Layer
The database is Amazon RDS MySQL. It is placed inside private database subnets.
Only the backend security group can connect to the database. This means the database is not publicly accessible.
AWS Networking Design
The VPC was designed with multiple subnet layers:
VPC
├── Public Subnets
│ ├── Public Frontend ALB
│ └── Frontend EC2 instances
│
├── Private App Subnets
│ ├── Internal Backend ALB
│ └── Backend EC2 instances
│
└── Private DB Subnets
├── RDS MySQL
This structure improved the security of the application because only the required parts were exposed publicly.
The frontend load balancer receives public HTTPS traffic. The backend load balancer is internal. The RDS database is private.
HTTPS and Custom Domain
For the domain setup, I used:
- Route 53 for DNS
- AWS Certificate Manager for SSL/TLS certificate
- HTTPS listener on the public frontend Application Load Balancer
The application can be accessed using:
This helped me understand how domain routing and HTTPS are configured in a real AWS environment.
Nginx Reverse Proxy Setup
One of the most important parts of this project was the Nginx reverse proxy.
The React frontend uses:
VITE_API_URL=/api
This means the browser sends API requests like this:
Then Nginx forwards those /api requests to the internal backend ALB.
Example Nginx concept:
location /api/ {
proxy_pass http://internal-backend-alb-dns/api/;
}
This solved a major production issue.
At first, the frontend was trying to call the internal backend ALB directly from the browser. That caused a timeout because the internal ALB is private and cannot be accessed from the public internet.
The correct solution was:
Browser
→ Public domain /api
→ Nginx reverse proxy
→ Internal backend ALB
→ Backend EC2
→ Private RDS
CI/CD with GitHub Actions
After completing the manual deployment, I added GitHub Actions to automate deployments.
I created separate deployment workflows for:
- Frontend deployment
- Backend deployment
Frontend CI/CD Flow
Developer pushes frontend code
↓
GitHub Actions starts
↓
Install dependencies
↓
Build React app using VITE_API_URL=/api
↓
Upload dist files to frontend EC2 using SCP
↓
Copy files to /usr/share/nginx/html
↓
Reload Nginx
↓
Validate frontend health endpoint
Frontend deployment proof:
Backend CI/CD Flow
The backend was more challenging because the backend EC2 instances are inside private subnets.
GitHub Actions cannot directly SSH into a private EC2 instance. To solve this, I used the frontend EC2 instance as a jump host.
GitHub Actions
↓
Frontend EC2 public instance
↓
Backend EC2 private instance
Backend deployment flow:
Developer pushes backend code
↓
GitHub Actions starts
↓
SSH into frontend EC2 jump host
↓
SSH into private backend EC2
↓
Upload backend source code
↓
Install production dependencies
↓
Restart backend using PM2
↓
Validate backend health endpoint
Backend deployment proof:
PM2 for Backend Process Management
The backend Node.js application was managed using PM2.
PM2 helped me keep the backend running continuously and restart it after deployment.
Useful PM2 commands used:
pm2 start server.js --name blog-backend pm2 restart blog-backend pm2 status pm2 logs
PM2 process proof:
Security Design
Security was one of the main points of this project.
Here is how the security design was planned:
Component => Security Approach
Frontend ALB => Public HTTPS access
Frontend EC2 => Receives traffic through frontend ALB
Backend ALB => Internal only
Backend EC2 => Private subnet, no direct public access
RDS MySQL => Private database subnet
Database access => Only backend security group allowed
API access => Browser uses /api through Nginx reverse proxy
SSL => ACM certificate with HTTPSSecrets Stored as environment variables, not committed to GitHub
This helped me understand the importance of layered security in cloud deployment.
Scalability and High Availability
This project includes production-style scalability components:
Frontend Auto Scaling Group
Backend Auto Scaling Group
Public Application Load Balancer
Internal Application Load Balancer
Multi-subnet architecture
Health checks for frontend and backend target groups
Private application and database layers
The Auto Scaling Groups allow EC2 instances to be replaced or scaled based on configuration. The load balancers distribute traffic to healthy targets.
Challenges I Faced and Solved
1. Browser Could Not Access Internal Backend ALB
Problem:
ERR_CONNECTION_TIMED_OUT
Reason:
The React frontend was built using the internal backend ALB DNS. Since the internal backend ALB is private, the browser could not access it.
Solution:
I changed the frontend API URL to:
VITE_API_URL=/api
Then Nginx reverse proxy forwards /api requests to the internal backend ALB.
2. Nginx Default Server Block Conflict
Problem:
Nginx had a conflicting default server block.
Solution:
I removed/commented the default server block and used only the project-specific Nginx configuration file.
Then I verified the configuration using:
sudo nginx -t
sudo systemctl reload nginx
3. Backend Private Subnet Deployment
Problem:
Backend EC2 instances were in private subnets, so GitHub Actions could not SSH directly into them.
Solution:
I used the frontend EC2 instance as a jump host.
GitHub Actions
↓
Frontend EC2 public instance
↓
Backend EC2 private instance
This helped me understand how private server deployment works in real production environments.
Live application home page:
Register page:
** Dashboard:**
Health Check Endpoints
Frontend health endpoint:
Backend health endpoint through public domain:
Backend local health endpoint:
Health checks were useful for verifying whether the frontend and backend were running correctly after deployment.
What I Learned
Through this project, I practiced and implemented:
- Designing AWS 3-tier architecture
- Deploying frontend and backend on separate layers
- Using public and private subnets correctly
- Configuring Route 53 custom domain
- Enabling HTTPS using ACM
- Creating public and internal Application Load Balancers
- Configuring Auto Scaling Groups
- Hosting React production build with Nginx
- Reverse proxying API requests through Nginx
- Running Node.js backend using PM2
- Connecting backend to private RDS MySQL
- Debugging ALB, Nginx, API, and security group issues
- Creating GitHub Actions CI/CD pipelines
- Deploying to private EC2 through a jump host
- Understanding real-world production deployment flow
- Future Improvements
GitHub Repository:
Live Demo:
Thanks for reading. Feedback and suggestions are welcome.


















Top comments (0)