I recently built a full-stack Hospital Management System and deployed it on AWS with a complete CI/CD pipeline. In this article I'll walk you through exactly how I did it — every command, every step, every mistake I made along the way.
By the end of this article you'll know how to:
- Containerize a MERN app with Docker
- Push images to AWS ECR
- Deploy backend on EC2
- Host frontend on S3
- Set up an Application Load Balancer
- Configure Auto Scaling
- Automate everything with Jenkins CI/CD
🏗️ Architecture Overview
GitHub Push → Jenkins CI/CD
├── Build Docker Images
├── Push to AWS ECR
├── Deploy Backend → EC2 (via SSM)
└── Deploy Frontend → S3
Users → S3 (Frontend) → ALB → EC2 (Backend) → MongoDB Atlas
Tech Stack:
- Frontend: React.js + Vite + Tailwind CSS
- Backend: Node.js + Express.js + Socket.IO
- Database: MongoDB Atlas
- Storage: Cloudinary
- AI: Groq API
📋 Prerequisites
Before starting, make sure you have:
- An AWS account (free tier works fine)
- Docker Desktop installed
- Node.js installed
- Jenkins installed locally
- Your MERN app code on GitHub
Phase 1 — AWS Account Setup
Create IAM Role for EC2
Your EC2 server needs permissions to talk to other AWS services. Instead of using secret keys on the server, we attach a Role.
- Go to IAM → Roles → Create role
- Select AWS service → EC2
- Attach these 4 policies:
AmazonS3FullAccessAmazonEC2ContainerRegistryFullAccessAmazonSSMManagedInstanceCoreCloudWatchAgentServerPolicy
- Name it:
EC2-Hospital-Role
Create IAM User for CLI
- IAM → Users → Create user → name:
hospital-cli-user - Attach policies:
AmazonEC2ContainerRegistryFullAccessAmazonS3FullAccessAmazonSSMFullAccessAmazonEC2FullAccessAWSCloudFormationFullAccess
- Security credentials → Create access key → CLI
- Download the CSV — you can't see the secret key again!
Phase 2 — AWS CLI Setup
Install AWS CLI from: https://awscli.amazonaws.com/AWSCLIV2.msi
Then configure it:
aws configure
# AWS Access Key ID: your-access-key
# AWS Secret Access Key: your-secret-key
# Default region name: eu-north-1
# Default output format: json
Verify it works:
aws sts get-caller-identity
You should see your Account ID. ✅
Phase 3 — ECR (Elastic Container Registry)
ECR is AWS's private Docker Hub. We create 2 repositories — one for backend, one for frontend.
aws ecr create-repository --repository-name hospital-backend --region eu-north-1
aws ecr create-repository --repository-name hospital-frontend --region eu-north-1
Login to ECR from your local machine:
aws ecr get-login-password --region eu-north-1 | docker login --username AWS --password-stdin YOUR_ACCOUNT_ID.dkr.ecr.eu-north-1.amazonaws.com
Build and push your Docker images:
# Build
docker-compose build
# Tag
docker tag hospital-app-backend:latest YOUR_ACCOUNT_ID.dkr.ecr.eu-north-1.amazonaws.com/hospital-backend:latest
docker tag hospital-app-frontend:latest YOUR_ACCOUNT_ID.dkr.ecr.eu-north-1.amazonaws.com/hospital-frontend:latest
# Push
docker push YOUR_ACCOUNT_ID.dkr.ecr.eu-north-1.amazonaws.com/hospital-backend:latest
docker push YOUR_ACCOUNT_ID.dkr.ecr.eu-north-1.amazonaws.com/hospital-frontend:latest
Phase 4 — EC2 Setup
Launch EC2 Instance
- EC2 → Launch instance
- Name:
hospital-backend-server - AMI: Amazon Linux 2023
- Instance type: t3.micro (free tier)
- Create key pair:
hospital-key(.pem format) — save this file! - Security group — open ports: 22, 80, 5000
- IAM instance profile:
EC2-Hospital-Role
Install Docker on EC2
Connect via EC2 Instance Connect, then run:
sudo yum update -y
sudo yum install -y docker
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker ec2-user
newgrp docker
Run Backend Container
# Login to ECR from EC2
aws ecr get-login-password --region eu-north-1 | docker login --username AWS --password-stdin YOUR_ACCOUNT_ID.dkr.ecr.eu-north-1.amazonaws.com
# Create .env file
cat > /home/ec2-user/.env << 'EOF'
PORT=5000
MONGO_URI=your_mongodb_uri
JWT_SECRET=your_jwt_secret
# ... add all your env vars
EOF
# Run container
docker run -d \
--name hospital-backend \
--restart always \
-p 5000:5000 \
--env-file /home/ec2-user/.env \
YOUR_ACCOUNT_ID.dkr.ecr.eu-north-1.amazonaws.com/hospital-backend:latest
Verify it's running:
docker logs hospital-backend
# Should see: Server running on port 5000 ✅
# Should see: MongoDB Connected! ✅
Phase 5 — S3 Frontend Hosting
# Create bucket
aws s3 mb s3://your-app-frontend --region eu-north-1
In AWS Console:
- S3 → your bucket → Properties → Static website hosting → Enable
- Index document:
index.html, Error document:index.html - Permissions → Block public access → untick all
- Add bucket policy:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-app-frontend/*"
}]
}
Build and upload frontend:
# Set your backend URL in frontend/.env
echo "VITE_API_URL=http://YOUR_ALB_DNS/api" > frontend/.env
# Build
cd frontend && npm run build
# Upload
aws s3 sync dist/ s3://your-app-frontend --region eu-north-1 --delete
Phase 6 — Application Load Balancer
WHY: The ALB gives you a stable DNS name that never changes even if EC2 restarts. It also enables Auto Scaling.
-
EC2 → Target Groups → Create
- Name:
hospital-tg - Protocol: HTTP, Port: 5000
- Health check path:
/ - Register your EC2 instance
- Name:
-
EC2 → Load Balancers → Create → Application Load Balancer
- Name:
hospital-alb - Internet-facing, all availability zones
- Listener: HTTP:80 → forward to
hospital-tg
- Name:
Wait for Active status, then copy the DNS name.
Update your frontend .env with the ALB DNS and redeploy to S3.
Phase 7 — Auto Scaling
Auto Scaling automatically adds more EC2 instances when CPU is high and removes them when traffic drops.
- Create AMI from your running EC2 instance
- Create Launch Template using that AMI
- Create Auto Scaling Group:
- Min: 1, Desired: 1, Max: 3
- CPU tracking policy: 50%
- Attach to your ALB target group
Phase 8 — Jenkins CI/CD Pipeline
WHY: Every time you push code, Jenkins automatically builds, pushes, and deploys everything. No manual work needed.
Add credentials in Jenkins:
-
aws-access-key(Secret text) -
aws-secret-key(Secret text)
Jenkinsfile:
pipeline {
agent any
environment {
AWS_ACCESS_KEY_ID = credentials('aws-access-key')
AWS_SECRET_ACCESS_KEY = credentials('aws-secret-key')
AWS_REGION = 'eu-north-1'
AWS_ACCOUNT_ID = 'YOUR_ACCOUNT_ID'
ECR_BACKEND = 'YOUR_ACCOUNT_ID.dkr.ecr.eu-north-1.amazonaws.com/hospital-backend'
ECR_FRONTEND = 'YOUR_ACCOUNT_ID.dkr.ecr.eu-north-1.amazonaws.com/hospital-frontend'
S3_BUCKET = 'your-app-frontend'
}
stages {
stage('Checkout') {
steps {
git branch: 'main', url: 'https://github.com/YOUR_USERNAME/YOUR_REPO.git'
}
}
stage('Build Images') {
steps {
bat 'docker build -t hospital-backend ./backend'
bat 'docker build -t hospital-frontend ./frontend'
}
}
stage('Push to ECR') {
steps {
bat '"C:\\Program Files\\Amazon\\AWSCLIV2\\aws.exe" ecr get-login-password --region %AWS_REGION% | docker login --username AWS --password-stdin %AWS_ACCOUNT_ID%.dkr.ecr.%AWS_REGION%.amazonaws.com'
bat 'docker tag hospital-backend:latest %ECR_BACKEND%:latest'
bat 'docker tag hospital-frontend:latest %ECR_FRONTEND%:latest'
bat 'docker push %ECR_BACKEND%:latest'
bat 'docker push %ECR_FRONTEND%:latest'
}
}
stage('Deploy Backend via SSM') {
steps {
script {
def instanceId = bat(
returnStdout: true,
script: '"C:\\Program Files\\Amazon\\AWSCLIV2\\aws.exe" ec2 describe-instances --filters "Name=tag:Name,Values=hospital-backend-server" "Name=instance-state-name,Values=running" --query "Reservations[0].Instances[0].InstanceId" --output text --region %AWS_REGION%'
).trim().readLines().last()
bat """
"C:\\Program Files\\Amazon\\AWSCLIV2\\aws.exe" ssm send-command ^
--instance-ids ${instanceId} ^
--document-name "AWS-RunShellScript" ^
--parameters "commands=['aws ecr get-login-password --region eu-north-1 | docker login --username AWS --password-stdin %AWS_ACCOUNT_ID%.dkr.ecr.eu-north-1.amazonaws.com && docker pull %ECR_BACKEND%:latest && docker stop hospital-backend || true && docker rm hospital-backend || true && docker run -d --name hospital-backend --restart always -p 5000:5000 --env-file /home/ec2-user/.env %ECR_BACKEND%:latest']" ^
--region %AWS_REGION%
"""
}
}
}
stage('Deploy Frontend to S3') {
steps {
bat 'cd frontend && npm install && npm run build'
bat '"C:\\Program Files\\Amazon\\AWSCLIV2\\aws.exe" s3 sync frontend\\dist\\ s3://%S3_BUCKET% --region %AWS_REGION% --delete'
}
}
}
post {
success { echo 'Deployment successful!' }
failure { echo 'Deployment failed!' }
}
}
Key lesson: Keep secrets out of code!
Use Jenkins credentials for all secrets. GitHub will block your push if it detects API keys in your code (learned this the hard way! 😅).
Phase 9 — CloudFormation (Infrastructure as Code)
Define your infrastructure in a YAML file so you can recreate everything with one command:
AWSTemplateFormatVersion: '2010-09-09'
Description: Hospital App Infrastructure
Resources:
BackendRepository:
Type: AWS::ECR::Repository
Properties:
RepositoryName: hospital-backend
FrontendBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: your-app-frontend
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: index.html
HospitalSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: hospital-sg
GroupDescription: Security group for Hospital App
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 5000
ToPort: 5000
CidrIp: 0.0.0.0/0
Deploy with:
aws cloudformation deploy \
--template-file cloudformation.yml \
--stack-name hospital-stack \
--region eu-north-1
🐛 Mistakes I Made (So You Don't Have To)
1. Secrets in Jenkinsfile
I accidentally put my Groq API key directly in the Jenkinsfile. GitHub blocked the push immediately. Always use Jenkins credentials instead of hardcoding secrets.
2. Wrong VITE_API_URL
After deployment the frontend was hitting localhost:5000 instead of the ALB URL. Always double check your .env before building.
3. Missing IAM permissions
I kept getting UnauthorizedOperation errors because my IAM user was missing EC2 and CloudFormation permissions. Add all needed permissions upfront.
4. SSM parameter parsing
Passing environment variables through SSM commands with special characters caused parsing errors. The fix was using --env-file on EC2 instead of passing each variable individually.
🎯 Results
After all this setup, my CI/CD pipeline works like this:
- I push code to GitHub
- Jenkins automatically triggers
- Docker images built and pushed to ECR
- Backend deployed to EC2 via SSM (no SSH needed!)
- Frontend built and uploaded to S3
- Everything live in ~10 minutes
No manual deployment steps. Ever.
📊 Cost
Running this on AWS free tier costs approximately $0/month if you:
- Use t3.micro EC2 (750 hours/month free)
- Stop EC2 when not using
- S3 storage is practically free for a small app
🔗 Resources
- GitHub Repo: https://github.com/abinash1417/Hospital-app
- Live Demo: http://hospital-app-frontend-159372.s3-website.eu-north-1.amazonaws.com
- AWS Free Tier: https://aws.amazon.com/free
Conclusion
Deploying a full-stack app on AWS from scratch taught me more about DevOps than any tutorial. The key things I learned:
- Always use IAM roles instead of access keys on servers
- Keep secrets in credential managers, never in code
- Load Balancers are essential for stable URLs
- CI/CD pipelines save enormous amounts of time
- Infrastructure as Code means you never lose your setup again
If you have any questions, drop them in the comments! Happy to help. 🚀
If this helped you, please leave a ❤️ and follow for more DevOps content!
Top comments (0)