Deploying a Scalable MERN Stack Application Using Multi-VM Setup with Vagrant

Aryan GuptaAryan Gupta
5 min read

As applications scale, so does the need for a reliable and scalable deployment environment. In this blog, I'll guide you through deploying a MERN (MongoDB, Express, React, Node.js) stack application using a multi-VM setup with Vagrant. This approach allows us to divide our application into separate virtual machines for the frontend, backend, and database, ensuring better scalability and fault isolation.

Why Use Multi-VM Deployment?

Deploying each part of your application to separate virtual machines allows you to scale individual components independently. For example, if your frontend faces heavy traffic, you can scale that VM without impacting your backend or database performance.

This blog will cover:

  • Setting up Vagrant for multi-VM deployments

  • Configuring Nginx as a reverse proxy

  • Setting up the MERN stack across VMs

  • Connecting the frontend, backend, and database

Prerequisites

Before you get started, you need:

  • Basic understanding of the MERN stack (MongoDB, Express, React, Node.js)

  • Installed Vagrant and VirtualBox on your local machine

  • Familiarity with Linux commands

1. Project Structure

We’ll divide the MERN stack as follows:

  1. Frontend (React): A dedicated VM serving the React frontend.

  2. Backend (Node.js + Express): A separate VM running Node.js and Express for API calls.

  3. Database (MongoDB): A VM hosting MongoDB.

We’ll use Nginx on both frontend and backend VMs as a reverse proxy for traffic routing.

2. Setting Up the Vagrantfile

The Vagrantfile is a configuration file used by Vagrant to define how VMs are set up and provisioned. Below is a multi-VM configuration for our MERN stack:

Vagrant.configure("2") do |config|

  config.vm.define "ngnix_frontend" do |ngnix_frontend|
    ngnix_frontend.vm.box = "ubuntu/bionic64"
    ngnix_frontend.vm.network "private_network", ip: "192.168.56.101"
    ngnix_frontend.vm.hostname = "ngnixFrontend"
    ngnix_frontend.vm.provision "shell", inline: <<-SHELL
      sudo apt-get update
      sudo apt-get install -y nginx
      sudo rm /etc/nginx/sites-enabled/default
      sudo bash -c 'cat <<EOF > /etc/nginx/sites-available/frontend
server {
    listen 80;
    server_name frontend.local;

    location / {
        proxy_pass http://192.168.56.102:3000;
    }
}
EOF'
      sudo ln -s /etc/nginx/sites-available/frontend /etc/nginx/sites-enabled/frontend
      sudo systemctl restart nginx
    SHELL
  end

  # Frontend VM
  config.vm.define "frontend" do |frontend|
    frontend.vm.box="ubuntu/bionic64"
    frontend.vm.network "private_network", ip:"192.168.56.102"
    frontend.vm.hostname ="frontend"
    frontend.vm.provision "shell", inline: <<-SHELL
      sudo apt-get update
      sudo apt-get install -y curl
      curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
      sudo apt-get install -y nodejs
      # Set up the frontend (React app)
      mkdir -p /home/vagrant/frontend
      cd /home/vagrant/frontend
      npx create-react-app myapp
      cd myapp
      npm start
    SHELL
  end

  config.vm.define "nginx_backend_proxy" do |nginx_backend_proxy|
    nginx_backend_proxy.vm.box = "ubuntu/bionic64"
    nginx_backend_proxy.vm.network "private_network", ip: "192.168.56.103"
    nginx_backend_proxy.vm.hostname = "nginx-backend-proxy"
    nginx_backend_proxy.vm.provision "shell", inline: <<-SHELL
      sudo apt-get update
      sudo apt-get install -y nginx
      # Configure Nginx to reverse proxy to Backend
      sudo rm /etc/nginx/sites-enabled/default
      sudo bash -c 'cat <<EOF > /etc/nginx/sites-available/backend
server {
    listen 80;
    server_name backend.local;

    location / {
        proxy_pass http://192.168.56.104:5000;
    }
}
EOF'
      sudo ln -s /etc/nginx/sites-available/backend /etc/nginx/sites-enabled/backend
      sudo systemctl restart nginx
    SHELL
  end

  # VM 4: Backend (Node.js)
  config.vm.define "backend" do |backend|
    backend.vm.box = "ubuntu/bionic64"
    backend.vm.network "private_network", ip: "192.168.56.104"
    backend.vm.hostname = "backend"
    backend.vm.provision "shell", inline: <<-SHELL
      sudo apt-get update
      sudo apt-get install -y curl
      curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
      sudo apt-get install -y nodejs
      sudo apt-get install -y mongodb
      # Set up the backend (Node.js)
      mkdir -p /home/vagrant/backend
      cd /home/vagrant/backend
      npm init -y
      npm install express mongoose body-parser
      # Create a simple backend API in Express
      echo "const express = require('express'); 
            const app = express();
            const bodyParser = require('body-parser');
            const mongoose = require('mongoose');
            mongoose.connect('mongodb://192.168.56.105/mydb', { useNewUrlParser: true, useUnifiedTopology: true });

            app.use(bodyParser.json());

            app.get('/api', (req, res) => {
              res.send('API is working');
            });

            const port = 5000;
            app.listen(port, () => {
              console.log('Server is running on port', port);
            });" > app.js
      node app.js
    SHELL
  end

  # VM 5: Database (MongoDB)
  config.vm.define "database" do |database|
    database.vm.box = "centos/7"
    database.vm.network "private_network", ip: "192.168.56.105"
    database.vm.hostname = "database"
    database.vm.provision "shell", inline: <<-SHELL
      sudo yum update -y
      sudo yum install -y mongodb-org
      sudo systemctl start mongod
      sudo systemctl enable mongod
      # Create a simple database and user
      mongo --eval "use mydb"
    SHELL
  end
end

3. Configuring Nginx as a Reverse Proxy

Nginx on the Frontend VM

Nginx on the frontend VM will act as a reverse proxy, routing requests to the React app and forwarding API requests to the backend VM.

server {
    listen 80;
    server_name frontend.local;

    location / {
        proxy_pass http://192.168.56.102:3000;  # Proxy to React frontend
    }
}

Nginx on the Backend VM

Nginx on the backend VM will forward requests to the Node.js server.

server {
    listen 80;
    server_name backend.local;

    location / {
        proxy_pass http://localhost:5000;  # Proxy to Node.js backend
    }
}

Make sure you place these configurations in /etc/nginx/sites-available/ and create symbolic links in /etc/nginx/sites-enabled/.

4. Connecting the MERN Stack

  • The frontend VM serves the React app and forwards API requests to the backend VM.

  • The backend VM communicates with MongoDB hosted on the database VM.

  • Ensure MongoDB is bound to the IP 192.168.56.104 in the MongoDB configuration file /etc/mongod.conf.

MongoDB Connection in Backend

In your Node.js backend, update the MongoDB connection string to point to the database VM’s private IP:

mongoose.connect('mongodb://192.168.56.104:27017/your-db-name', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

5. Bringing Up the VMs

Once you have defined your Vagrantfile and configured Nginx, bring up the VMs by running:

vagrant up

This will spin up all three VMs (frontend, backend, and database), and you can now access your MERN application.

To SSH into individual VMs, use:

vagrant ssh frontend
vagrant ssh backend
vagrant ssh database

6. Testing the Application

Once all VMs are running, open a browser and go to the frontend.local domain (you may need to update your hosts file to resolve this domain locally). The React frontend will be served, and all API requests will be proxied to the backend.

Make sure to check each VM’s services:

  • Ensure MongoDB is running on the database VM.

  • Ensure the React app is built and running on the frontend VM.

  • Ensure the Node.js server is handling API requests on the backend VM.

Conclusion

Deploying a MERN application using multiple VMs with Vagrant gives you greater flexibility, especially when scaling and isolating different components. With this setup, each part of your stack can be independently scaled and managed, which is a significant advantage when handling increased traffic or scaling out services.

If you’re looking to scale your application or experiment with distributed architectures, Vagrant and a multi-VM setup provide a simple yet powerful solution for MERN stack deployments.

0
Subscribe to my newsletter

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

Written by

Aryan Gupta
Aryan Gupta