Building and Dockerizing a MERN Stack Application

Pratik Nalawade - Aug 8 - - Dev Community

The MERN stack (MongoDB, Express.js, React, Node.js) is a powerful technology stack for building modern web applications. In this blog post, we’ll walk through the process of creating a simple Todo application using the MERN stack and then dockerizing it for easy deployment and scalability.

Image description

A Closer Look at MERN Stack Components
MongoDB: A cross-platform document database
MongoDB is a NoSQL (non-relational) document-oriented database. Data is stored in flexible documents with a JSON (JavaScript Object Notation)-based query language. MongoDB is known for being flexible and easy to scale.

Express: A back-end web application framework
Express is a web application framework for Node.js, another MERN component. Instead of writing full web server code by hand on Node.js directly, developers use Express to simplify the task of writing server code.

React: A JavaScript library for building user interfaces
React was originally created by a software engineer at Facebook, and was later open-sourced. The React library can be used for creating views rendered in HTML. In a way, it’s the defining feature of the stack.

Node.js: A cross-platform JavaScript runtime environment
Node.js is constructed on Chrome’s V8 JavaScript engine. It’s designed to build scalable network applications and can execute JavaScript code outside of a browser.

Prerequisites
Before we start, ensure you have the following installed on your machine:

Node.js and npm
MongoDB
Docker

Step 1: Setting Up the Project
First, create a directory for your project and initialize it:

mkdir merntodo
cd merntodo
Enter fullscreen mode Exit fullscreen mode

Step 2: Creating the Backend with Node.js and Express
Initialize the backend:

mkdir server
cd server
npm init -y
npm install express mongoose cors body-parser
Enter fullscreen mode Exit fullscreen mode

Create a basic Express server (server/server.js):

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const bodyParser = require('body-parser');
const app = express();
const PORT = process.env.PORT || 5000;
app.use(cors());
app.use(bodyParser.json());
const dbURI = process.env.MONGO_URI || 'mongodb://localhost:27017/mern-todo';
mongoose.connect(dbURI, {
  useNewUrlParser: true,
  useUnifiedTopology: true
}).then(() => console.log('MongoDB connected'))
  .catch(err => console.log(err));
const todoRoutes = require('./routes/todo');
app.use('/api', todoRoutes);
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Enter fullscreen mode Exit fullscreen mode

use your db URL in const dbURI = process.env.MONGO_URI ||‘mongodb://localhost:27017/mern-todo’;

Define the Todo model (server/models/Todo.js):

const mongoose = require('mongoose');
const todoSchema = new mongoose.Schema({
  text: {
    type: String,
    required: true
  },
  completed: {
    type: Boolean,
    default: false
  }
});
module.exports = mongoose.model('Todo', todoSchema);

Enter fullscreen mode Exit fullscreen mode

Create routes for the Todo API (server/routes/todo.js):

const express = require('express');
const router = express.Router();
const Todo = require('../models/Todo');
// Get all todos
router.get('/todos', async (req, res) => {
  try {
    const todos = await Todo.find();
    res.json(todos);
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

// Create a new todo
router.post('/todos', async (req, res) => {
  const todo = new Todo({
    text: req.body.text
  });
  try {
    const newTodo = await todo.save();
    res.status(201).json(newTodo);
  } catch (err) {
    res.status(400).json({ message: err.message });
  }
});
// Delete a todo
router.delete('/todos/:id', async (req, res) => {
  try {
    const todo = await Todo.findById(req.params.id);
    if (!todo) {
      return res.status(404).json({ message: 'Todo not found' });
    }
    await todo.remove();
    res.json({ message: 'Todo deleted' });
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});
module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Step 3: Creating the Frontend with React
Initialize the frontend:

npx create-react-app client
cd client

Enter fullscreen mode Exit fullscreen mode

Create a Todo List component (client/src/TodoList.js):

import React, { useState, useEffect } from 'react';
import axios from 'axios';
const TodoList = () => {
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState('');
  useEffect(() => {
    fetchTodos();
  }, []);
  const fetchTodos = async () => {
    try {
      const res = await axios.get('http://localhost:5000/api/todos');
      setTodos(res.data);
    } catch (err) {
      console.error(err);
    }
  };
  const createTodo = async () => {
    try {
      const res = await axios.post('http://localhost:5000/api/todos', { text: newTodo });
      setTodos([...todos, res.data]);
      setNewTodo('');
    } catch (err) {
      console.error(err);
    }
  };
  const deleteTodo = async (id) => {
    try {
      await axios.delete(`http://localhost:5000/api/todos/${id}`);
      setTodos(todos.filter(todo => todo._id !== id));
    } catch (err) {
      console.error(err);
    }
  };
  return (
    <div>
      <h1>Todo List</h1>
      <input 
        type="text" 
        value={newTodo} 
        onChange={(e) => setNewTodo(e.target.value)} 
      />
      <button onClick={createTodo}>Add Todo</button>
      <ul>
        {todos.map(todo => (
          <li key={todo._id}>
            {todo.text}
            <button onClick={() => deleteTodo(todo._id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
};
export default TodoList;

Enter fullscreen mode Exit fullscreen mode

Update client/src/App.js to include the Todo List component:

import React from 'react';
import TodoList from './TodoList';
function App() {
  return (
    <div className="App">
      <TodoList />
    </div>
  );
}
export default App;

Enter fullscreen mode Exit fullscreen mode

Step 4: Dockerizing the Application
Create Dockerfiles
Backend Dockerfile (server/Dockerfile):

# Use the official Node.js image as the base image
FROM node:14
# Create and set the working directory
WORKDIR /usr/src/app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Expose the port the app runs on
EXPOSE 5000
# Start the application
CMD ["node", "server.js"]

Enter fullscreen mode Exit fullscreen mode

Frontend Dockerfile (client/Dockerfile):

# Use the official Node.js image as the base image
FROM node:14
# Create and set the working directory
WORKDIR /usr/src/app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Build the React app
RUN npm run build
# Install serve to serve the production build
RUN npm install -g serve
# Expose the port the app runs on
EXPOSE 3000
# Start the application
CMD ["serve", "-s", "build"]
Create Docker Compose File (docker-compose.yml)
version: '3.8'
services:
  backend:
    build:
      context: ./server
      dockerfile: Dockerfile
    ports:
      - "5000:5000"
    environment:
      - NODE_ENV=development
      - MONGO_URI=mongodb://mongo:27017/mern-todo
    depends_on:
      - mongo
  frontend:
    build:
      context: ./client
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    depends_on:
      - backend
  mongo:
    image: mongo:4.2
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db
volumes:
  mongo-data:

Enter fullscreen mode Exit fullscreen mode

Step 5: Build and Run Docker Containers
From the root directory of your project, run the following commands to build and start your Docker containers:

docker-compose build
docker-compose up
Enter fullscreen mode Exit fullscreen mode

Step 6: Access Your Application
Frontend: Open your browser and navigate to http://localhost:3000
Backend: The backend API will be running at http://localhost:5000
Conclusion
By following these steps, you have successfully built a MERN stack Todo application and dockerized it for easy deployment. Dockerizing your application ensures that it runs consistently across different environments and simplifies the deployment process. Happy coding!

code repo:github.com/PRATIKNALAWADE/Docker-mern

. . . . . . . . . . . . . .