Brainly - Case Study

Arnab SahaArnab Saha
5 min read

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

  1. 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.

  2. For scalability, we can also integrate VM Scale Sets (VMSS) on Azure to enable auto-scaling.

  3. 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
  1. 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.

  2. This build-time injection approach works well for static site generation, like Vite’s production builds.

Deployment

Make Azure VM

  1. Create an Azure Virtual Machine with:

    • Ubuntu 24.04 LTS

    • Enabled HTTPS, SSH, and HTTP during setup.

  2. Add an RSA SSH key during the VM setup process.

  3. Download the .pem file for secure access.

  4. Go to Networking > Inbound Port Rules and add the required ports (e.g., 3000, 3001, 3002, etc.).

  5. Set appropriate permissions on your .pem file:

     chmod 600 /Users/thearnabsaha/Downloads/keys/vm1_key.pem
    
  6. Connect to your VM via SSH:

     ssh -i "/Users/thearnabsaha/Downloads/keys/vm1_key.pem" azureuser@40.81.235.53
    

Nginx Config

  1. sudo apt update

  2. sudo apt install nginx

  3. sudo rm -rf /etc/nginx/nginx.conf

  4. 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;
        }
    }
}
  1. sudo systemctl restart nginx

SSL certificate

  1. sudo apt install certbot python3-certbot-nginx

  2. sudo certbot --nginx

  3. sudo nginx -t

  4. 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
  1. Create a folder for your project and navigate into it.

  2. Create a docker-compose.yml file:

  3. 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
  1. 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

  1. curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash

  2. source ~/.bashrc

  3. nvm install --lts

  4. node -v , npm -v

GitHub

  1. git clone https://github.com/thearnabsaha/Brainly.git

  2. add .env

  3. npm run dev (not build)

  4. npm run build

  5. npm start

Pm2

  1. npm install -g pm2

  2. pm2 start dist/index.js --name brainly-server

  3. few pm2 commands

TaskCommandDescription
Start a filepm2 start dist/index.jsStarts a Node.js app
Start with namepm2 start dist/index.js --name brainly-serverGive a custom name
Start with ecosystem filepm2 start ecosystem.config.jsLoad a full config (optional)
TaskCommandDescription
List all processespm2 listShows all running PM2 apps
Show detailed infopm2 show brainly-serverApp details
Restart apppm2 restart brainly-serverRestarts a specific app
Stop apppm2 stop brainly-serverStops it but keeps it in memory
Delete apppm2 delete brainly-serverRemoves from memory
TaskCommandDescription
Live logspm2 logsAll logs in real time
Logs for specific apppm2 logs brainly-serverFilter logs by name
Monitor performancepm2 monitLive CPU/RAM monitoring dashboard
TaskCommandDescription
Kill all appspm2 killStops PM2 and all processes
Remove all appspm2 delete allClean slate
Clear logspm2 flushClears all log files

CI/CD for github

  1. 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
1
Subscribe to my newsletter

Read articles from Arnab Saha directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Arnab Saha
Arnab Saha