Containerized MERN Application Deployment on AWS ECS
Designed and deployed a production-ready MERN application (React frontend + Express backend + MongoDB) as Docker containers on AWS ECS, with images hosted in ECR. Infrastructure is provisioned via Terraform with CI/CD implemented using GitHub Actions with a private self-hosted runner on AWS (OIDC-based), integrated with SonarQube for code quality checks.
Project Overview
This project demonstrates an end-to-end containerized deployment of a MERN stack application on AWS. The frontend (React) and backend (Express + MongoDB) are packaged as separate Docker images and pushed to Amazon ECR. Each image runs as an ECS task with separate task definitions for client and server components. An Application Load Balancer (ALB) routes traffic and exposes the services to the internet.
The infrastructure is completely managed using Terraform modules for reproducibility and scalability. CI/CD is implemented using GitHub Actions with a private self-hosted runner on EC2 that authenticates to AWS using OpenID Connect (OIDC) for secure, short-lived credentials. SonarQube is integrated in the pipeline for comprehensive static code analysis and quality gates.
Technical Architecture
Technology Stack
- Frontend: React (Create React App / Vite)
- Backend: Node.js + Express.js
- Database: MongoDB (containerized)
- Container Runtime: Docker
- Container Registry: Amazon ECR
- Orchestration: AWS ECS (Fargate)
- Load Balancer: Application Load Balancer
- DNS: Route53
- Infrastructure: Terraform
- CI/CD: GitHub Actions
- Code Quality: SonarQube
Project Specifications
- Duration: 4 weeks
- Role: DevOps + Full-stack Engineer
- Services: 2 ECS Services (Client & Server)
- Repositories: 2 ECR Repositories
- Runner: Self-hosted on EC2
- Security: OIDC-based authentication
- Monitoring: CloudWatch Logs & Metrics
- Deployment: Blue-Green with ALB
Containerization
Multi-stage Docker builds for optimized images
AWS ECS
Serverless container orchestration with Fargate
CI/CD Pipeline
Automated builds, tests, and deployments
Security
OIDC authentication and IAM best practices
Project Metrics & Impact
Challenges & Solutions
Challenge: Secure CI/CD Credentials and Least-Privilege Access
Solution: Implemented GitHub OIDC to mint short-lived AWS credentials for the self-hosted runner, applying narrowly scoped IAM roles for both the runner and ECS tasks to follow the principle of least privilege.
Challenge: Blue/Green Deployments with ECS
Solution: Leveraged ECS service deployment configurations and ALB listener rules to perform rolling deployments with gradual traffic shifting between task sets, ensuring zero-downtime deployments.
Challenge: Integrating SonarQube in CI Pipeline
Solution: Added SonarQube scan steps in GitHub Actions that send analysis results to the SonarQube server running in the AWS environment, with fail-fast mechanisms on critical code quality issues.
Challenge: ALB Routing for Multiple Services
Solution: Configured ALB listeners with path-based routing rules and separate target groups to efficiently route traffic between client and server containers based on request paths.
Development Timeline
Infrastructure Setup & Terraform Modules
Created Terraform modules for VPC, ECS cluster, ECR repositories, and ALB. Set up basic infrastructure components and networking.
Application Containerization & ECR Integration
Containerized React frontend and Express backend, optimized Docker images with multi-stage builds, and integrated with ECR for image storage.
ECS Services & ALB Configuration
Configured ECS task definitions for both services, set up ALB listeners and target groups, implemented health checks and auto-scaling policies.
CI/CD Pipeline & SonarQube Integration
Built GitHub Actions pipeline with self-hosted runner, integrated OIDC authentication, added SonarQube quality gates, and performed end-to-end testing.
Code & Pipeline Highlights
Terraform ECS Task Definition
{
"compatibilities": [
"EC2",
"FARGATE"
],
"containerDefinitions": [
{
"cpu": 0,
"environment": [],
"environmentFiles": [],
"essential": true,
"image": "471112857795.dkr.ecr.us-east-1.amazonaws.com/saeedafzal2030/thinkify_client:latest",
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/thinkify_client",
"awslogs-create-group": "true",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
},
"secretOptions": []
},
"mountPoints": [],
"name": "thikify_client",
"portMappings": [
{
"appProtocol": "http",
"containerPort": 80,
"hostPort": 80,
"name": "http",
"protocol": "tcp"
}
],
"systemControls": [],
"ulimits": [],
"volumesFrom": []
}
],
"cpu": "1024",
"enableFaultInjection": false,
"executionRoleArn": "arn:aws:iam::471112857795:role/ecsTaskExecutionRole",
"family": "thinkify_client",
"memory": "2048",
"networkMode": "awsvpc",
"placementConstraints": [],
"registeredAt": "2025-09-07T16:05:00.332Z",
"registeredBy": "arn:aws:iam::471112857795:user/TF",
"requiresAttributes": [
{
"name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
},
{
"name": "ecs.capability.execution-role-awslogs"
},
{
"name": "com.amazonaws.ecs.capability.ecr-auth"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
},
{
"name": "com.amazonaws.ecs.capability.task-iam-role"
},
{
"name": "ecs.capability.execution-role-ecr-pull"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
},
{
"name": "ecs.capability.task-eni"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.29"
}
],
"requiresCompatibilities": [
"FARGATE"
],
"revision": 1,
"runtimePlatform": {
"cpuArchitecture": "X86_64",
"operatingSystemFamily": "LINUX"
},
"status": "ACTIVE",
"taskDefinitionArn": "arn:aws:ecs:us-east-1:471112857795:task-definition/thinkify_client:5",
"taskRoleArn": "arn:aws:iam::471112857795:role/ecsTaskExecutionRole",
"volumes": [],
"tags": []
}
GitHub Actions CI/CD Pipeline
name: Intigration workflow
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
jobs:
frontend:
runs-on: self-hosted
environment: Dev
permissions:
contents: read
id-token: write # For OIDC authentication if needed in future
steps:
# 🔹 Clean workspace before checkout
- name: Clean workspace
run: |
echo "Cleaning workspace..."
echo $GITHUB_WORKSPACE
sudo rm -rf "$GITHUB_WORKSPACE"/* || true
sudo rm -rf "$GITHUB_WORKSPACE"/.[!.]* || true
# 🔹 Checkout repo with clean option
- name: Checkout code
uses: actions/checkout@v5
with:
clean: true
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Verify environment & list client folder
run: |
node --version
cd client
ls -ltr
- name: Install dependencies and audit
working-directory: ./client
run: |
rm -rf node_modules package-lock.json
npm install || true
npm audit fix || true
- name: Run tests
working-directory: ./client
run: npm test
- uses: SonarSource/sonarqube-scan-action@v5.3.1
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
with:
projectBaseDir: './client'
args: >
-Dsonar.projectKey=thinkify_cicd
-Dsonar.verbose=true
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4.3.1
with:
aws-region: us-east-1
role-to-assume: arn:aws:iam::471112857795:role/GithubAccessToECS
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and tag docker images
working-directory: ./client
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: saeedafzal2030/thinkify_client
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG .
docker tag $REGISTRY/$REPOSITORY:$IMAGE_TAG $REGISTRY/$REPOSITORY:latest
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
docker push $REGISTRY/$REPOSITORY:latest
- name: Run Trivy vulnerability scanner on docker image
uses: aquasecurity/trivy-action@0.28.0
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: saeedafzal2030/thinkify_client
IMAGE_TAG: ${{ github.sha }}
with:
scan-type: 'image'
image-ref: "${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}"
format: 'table'
output: 'thinkify-report-client-${{ github.sha }}'
exit-code: '0'
severity: 'MEDIUM,HIGH,CRITICAL'
- name: Upload Trivy Scan Reports
uses: actions/upload-artifact@v4
with:
name: 'thinkify-report-client-${{ github.sha }}'
path: 'thinkify-report-client-${{ github.sha }}'
if-no-files-found: 'warn'
retention-days: 7
- name: Docker cleanup
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: saeedafzal2030/thinkify_client
IMAGE_TAG: ${{ github.sha }}
run: |
docker rmi $REGISTRY/$REPOSITORY:$IMAGE_TAG
docker rmi $REGISTRY/$REPOSITORY:latest
outputs:
registry: ${{ steps.login-ecr.outputs.registry }}
backend:
runs-on: self-hosted
environment: Dev
permissions:
contents: read
id-token: write # For OIDC authentication if needed in future
steps:
# 🔹 Clean workspace before checkout
- name: Clean workspace
run: |
echo "Cleaning workspace..."
echo $GITHUB_WORKSPACE
sudo rm -rf "$GITHUB_WORKSPACE"/* || true
sudo rm -rf "$GITHUB_WORKSPACE"/.[!.]* || true
- name: Checkout code
uses: actions/checkout@v5
with:
clean: 'true'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies and audit
working-directory: ./server
run: |
node --version
npm install
npm audit fix || true
- name: Run tests
working-directory: ./server
run: npm test
# - uses: SonarSource/sonarqube-scan-action@v5.3.1
# env:
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
# with:
# projectBaseDir: './server'
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4.3.1
with:
aws-region: us-east-1
role-to-assume: arn:aws:iam::471112857795:role/GithubAccessToECS
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push docker image to Amazon ECR
working-directory: ./server
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: saeedafzal2030/thinkify_server
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG -t $REGISTRY/$REPOSITORY:latest .
docker tag $REGISTRY/$REPOSITORY:$IMAGE_TAG $REGISTRY/$REPOSITORY:latest
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
docker push $REGISTRY/$REPOSITORY:latest
- name: Run Trivy vulnerability scanner on docker image
uses: aquasecurity/trivy-action@0.28.0
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: saeedafzal2030/thinkify_server
IMAGE_TAG: ${{ github.sha }}
with:
scan-type: 'image'
image-ref: "${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}"
format: 'table'
output: 'thinkify-report-server-${{ github.sha }}'
exit-code: '0'
severity: 'MEDIUM,HIGH,CRITICAL'
- name: Upload Trivy Scan Reports
uses: actions/upload-artifact@v4
with:
name: 'thinkify-report-server-${{ github.sha }}'
path: 'thinkify-report-server-${{ github.sha }}'
if-no-files-found: 'warn'
retention-days: 7
- name: Docker cleanup
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: saeedafzal2030/thinkify_server
IMAGE_TAG: ${{ github.sha }}
run: |
docker rmi $REGISTRY/$REPOSITORY:$IMAGE_TAG
docker rmi $REGISTRY/$REPOSITORY:latest
outputs:
registry: ${{ steps.login-ecr.outputs.registry }}
Infrastructure & Application Gallery
AWS Infrastructure Components























