Brainly - Case Study

A Full-stack web application built with Vite + React, Express, and MongoDB Atlas, Containerized using Docker and hosted on an Azure VM.
Tech Stack
Frontend: Vite + React
Backend: Node.js + Express
Database: MongoDB Atlas
Containerization: Docker
Deployment: Azure VM (Ubuntu 24.04 LTS)
Web Server: Nginx (with HTTPS)
Project Structure
Brainly/
├── brainly-client/ # Vite + React
└── brainly-server/ # Express + Node.js
Each of these contains a Dockerfile.prod
.
Thinking Process
There are two main ways to run the project:
Method 1: Clone the repository from GitHub and run the app using
npm run dev
for local development.Method 2: Use Docker by setting up a
docker-compose.yml
file and deploying the containers — ideal for production and platform-independent environments.
For scalability, we can also integrate VM Scale Sets (VMSS) on Azure to enable auto-scaling.
Since Vite doesn't support runtime
.env
injection easily, we needed to pass the environment variable (VITE_API_BASE
) at build time. That’s why we built the Docker image using this command:
docker build --platform linux/amd64 -f brainly-client/Dockerfile.prod --build-arg VITE_API_BASE=https://brainlys.grevelops.co/ -t hparnab/brainly-client:latest ./brainly-client
The
--platform linux/amd64
flag is used because the image was built on an M1 Mac, which has an ARM-based architecture. This ensures compatibility with most Linux servers running on amd64.This build-time injection approach works well for static site generation, like Vite’s production builds.
Deployment
Make Azure VM
Create an Azure Virtual Machine with:
Ubuntu 24.04 LTS
Enabled HTTPS, SSH, and HTTP during setup.
Add an RSA SSH key during the VM setup process.
Download the
.pem
file for secure access.Go to Networking > Inbound Port Rules and add the required ports (e.g.,
3000
,3001
,3002
, etc.).Set appropriate permissions on your
.pem
file:chmod 600 /Users/thearnabsaha/Downloads/keys/vm1_key.pem
Connect to your VM via SSH:
ssh -i "/Users/thearnabsaha/Downloads/keys/vm1_key.pem" azureuser@40.81.235.53
Nginx Config
sudo apt update
sudo apt install nginx
sudo rm -rf /etc/nginx/nginx.conf
sudo nano /etc/nginx/nginx.conf
events {}
http {
server {
listen 80;
server_name brainlys.grevelops.co;
location / {
proxy_pass <http://localhost:3000>;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
server {
listen 80;
server_name brainly.grevelops.co;
location / {
proxy_pass <http://localhost:4173>;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
}
- sudo systemctl restart nginx
SSL certificate
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx
sudo nginx -t
sudo nginx -s reload
Docker Installation
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
//to check docker
docker --version
//to run docker
sudo systemctl enable docker
sudo systemctl start docker
//to run without the sudo command
sudo usermod -aG docker $USER
newgrp docker
docker compose version
sudo docker compose up --pull always
sudo docker compose up -d
sudo docker compose pull
sudo docker compose up --force-recreate
Create a folder for your project and navigate into it.
Create a
docker-compose.yml
file:Add the following configuration to your
docker-compose.yml
:
version: "3.9"
services:
backend:
image: hparnab/brainly-server:latest
platform: linux/amd64
ports:
- "3000:3000"
restart: unless-stopped
environment:
- NODE_ENV=production
- JWT_SECRET_KEY=<secretKey>
- MONGODB_URI=mongodb+srv://<username>:<password>@brainly.a9e87.mongodb.net/?retryWrites=true>
- CORS_ORIGIN=https://brainly.grevelops.co
frontend:
image: hparnab/brainly-client:latest
platform: linux/amd64
ports:
- "4173:4173"
restart: unless-stopped
- Start your Docker containers:
docker compose up -d
Useful Docker Commands
// **View logs for all containers**:
docker ps --format "table {{.Names}}" | tail -n +2 | xargs -I {} sh -c 'echo "=== {} ===" && docker logs {} --tail=50'
// **Access the shell of a container:**
docker exec -it brainly-app-backend-1 /bin/sh
// To remove **all Docker images**:
docker rmi $(docker images -q) --force
// To stop and remove all running and stopped containers:
docker stop $(docker ps -aq) 2>/dev/null || true && docker rm $(docker ps -aq) 2>/dev/null || true
Github Installation
Install Nodejs
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
source ~/.bashrc
nvm install --lts
node -v , npm -v
GitHub
add .env
npm run dev (not build)
npm run build
npm start
Pm2
npm install -g pm2
pm2 start dist/index.js --name brainly-server
few pm2 commands
Task | Command | Description |
Start a file | pm2 start dist/index.js | Starts a Node.js app |
Start with name | pm2 start dist/index.js --name brainly-server | Give a custom name |
Start with ecosystem file | pm2 start ecosystem.config.js | Load a full config (optional) |
Task | Command | Description |
List all processes | pm2 list | Shows all running PM2 apps |
Show detailed info | pm2 show brainly-server | App details |
Restart app | pm2 restart brainly-server | Restarts a specific app |
Stop app | pm2 stop brainly-server | Stops it but keeps it in memory |
Delete app | pm2 delete brainly-server | Removes from memory |
Task | Command | Description |
Live logs | pm2 logs | All logs in real time |
Logs for specific app | pm2 logs brainly-server | Filter logs by name |
Monitor performance | pm2 monit | Live CPU/RAM monitoring dashboard |
Task | Command | Description |
Kill all apps | pm2 kill | Stops PM2 and all processes |
Remove all apps | pm2 delete all | Clean slate |
Clear logs | pm2 flush | Clears all log files |
CI/CD for github
- touch .github/workflows/deploy.yaml
name: Deploy to Azure VM
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Set up SSH key
run: |
echo "${{ secrets.AZURE_KEY }}" > key.pem
chmod 600 key.pem
- name: Deploy to Azure VM
run: |
ssh -o StrictHostKeyChecking=no -i key.pem ${{ secrets.AZURE_USER }}@${{ secrets.AZURE_HOST }} << 'EOF'
cd /home/${{ secrets.AZURE_USER }}/Brainly
git pull origin main
EOF
Subscribe to my newsletter
Read articles from Arnab Saha directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
