๐ Comprehensive Tutorial: Setting Up MongoDB with Node.js Using Docker Compose
Outline
- ๐ ๏ธ Prerequisites
- ๐ Project Structure
- ๐ณ Setting Up Docker Compose
- ๐ Creating the Dockerfile
- ๐ฆ Building the Node.js Application
- ๐ Connecting Node.js to MongoDB
- ๐ฅ๏ธ Creating a Simple HTML Form
- ๐ Running the Application
- ๐งช Testing the Setup
- ๐งน Cleaning Up
- ๐ Conclusion
๐ ๏ธ Prerequisites
Before diving into the tutorial, ensure you have the following installed on your system:
- Docker: Docker allows you to containerize applications, making them portable and easy to manage.
- Docker Compose: A tool for defining and running multi-container Docker applications.
- Node.js: JavaScript runtime for building server-side applications.
- npm: Node package manager, usually installed alongside Node.js.
Note: Ensure Docker and Docker Compose are running properly by executing
docker --version
anddocker-compose --version
in your terminal.
๐ Project Structure
Organize your project directory as follows to maintain clarity and manageability:
mongodb-nodejs-docker/
โโโ docker-compose.yml
โโโ docker-compose.dev.yml
โโโ docker-compose.prod.yml
โโโ Dockerfile
โโโ .env
โโโ package.json
โโโ package-lock.json
โโโ mongo-data/
โโโ src/
โ โโโ index.js
โโโ README.md
๐ File Descriptions
File/Directory | Description |
---|---|
docker-compose.yml |
Base Docker Compose configuration for services. |
docker-compose.dev.yml |
Development-specific Docker Compose overrides. |
docker-compose.prod.yml |
Production-specific Docker Compose overrides. |
Dockerfile |
Defines how to build the Docker image for the Node.js application. |
.env |
Stores environment variables securely. |
package.json |
Lists the project's dependencies and scripts. |
package-lock.json |
Automatically generated for locking dependencies. |
mongo-data/ |
Persistent storage for MongoDB data. |
src/index.js |
Main Node.js application file. |
README.md |
Documentation and project overview. |
๐ณ Setting Up Docker Compose
Docker Compose allows you to manage multi-container Docker applications. We'll define two services: node-app
for the Node.js application and mongodb
for the MongoDB database.
๐ง docker-compose.yml (Base Configuration)
Create a docker-compose.yml
file in the root directory with the following content:
version: "3.8"
services:
node-app:
container_name: node-app-dev
build:
context: .
target: development
ports:
- "4002:4002"
env_file:
- ./.env
volumes:
- ./src:/app/src:ro
- /app/node_modules
depends_on:
- mongodb
command: npm run start-dev
mongodb:
container_name: mongo-dev
image: mongo:latest
restart: always
ports:
- "27017:27017"
volumes:
- mongo-db:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
volumes:
mongo-db:
๐ ๏ธ docker-compose.dev.yml (Development Overrides)
Create a docker-compose.dev.yml
for development-specific configurations:
version: "3.8"
services:
node-app:
environment:
- NODE_ENV=development
volumes:
- ./src:/app/src
- /app/node_modules
command: npm run start-dev
๐ ๏ธ docker-compose.prod.yml (Production Overrides)
Create a docker-compose.prod.yml
for production-specific configurations:
version: "3.8"
services:
node-app:
build:
target: production
environment:
- NODE_ENV=production
ports:
- "4002:4002"
command: npm run start
๐ Creating the Dockerfile
The Dockerfile
defines how to build the Docker image for your Node.js application. We'll use multi-stage builds to optimize the image for both development and production environments.
Dockerfile
Create a Dockerfile
in the root directory with the following content:
# Stage 1: Base Image
FROM node:18-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./
# Stage 2: Development Stage
FROM base AS development
ENV NODE_ENV=development
RUN npm install
COPY . .
CMD ["npm", "run", "start-dev"]
# Stage 3: Production Stage
FROM base AS production
ENV NODE_ENV=production
RUN npm install --only=production
COPY . .
CMD ["npm", "run", "start"]
๐ Explanation
Stage | Purpose | Commands |
---|---|---|
Base | Establishes the base environment with dependencies copied. | FROM node:18-alpine AS base WORKDIR /app COPY package.json package-lock.json ./ |
Development | Sets up the development environment with all dependencies installed. | FROM base AS development ENV NODE_ENV=development RUN npm install COPY . . CMD ... |
Production | Sets up the production environment with only production dependencies. | FROM base AS production ENV NODE_ENV=production RUN npm install --only=production COPY . . CMD ... |
Note: Multi-stage builds help in reducing the final image size by only including necessary files and dependencies for each environment.
๐ฆ Building the Node.js Application
We'll create a simple Node.js application using Express and Mongoose to interact with MongoDB.
Step 1: Initialize the Project
Navigate to your project directory and initialize a new Node.js project:
Step 2: Install Dependencies
Install the necessary dependencies:
- express: Web framework for Node.js.
- mongoose: ODM for MongoDB.
- nodemon: Utility that automatically restarts the server on file changes (development only).
Step 3: Update package.json Scripts
Modify the package.json
to include scripts for development and production:
{
"name": "mongodb-nodejs-docker",
"version": "1.0.0",
"description": "A simple Node.js app with MongoDB using Docker Compose",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"start-dev": "nodemon src/index.js"
},
"author": "Your Name",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"mongoose": "^6.7.0"
},
"devDependencies": {
"nodemon": "^2.0.20"
}
}
๐ Connecting Node.js to MongoDB
We'll set up Mongoose to connect to the MongoDB service defined in Docker Compose.
src/index.js
Create a src
directory and within it, create an index.js
file with the following content:
const express = require("express");
const mongoose = require("mongoose");
const path = require("path");
const app = express();
// Middleware to parse JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Serve the HTML form on the root route
app.get("/", (req, res) => {
res.send(`
<html>
<head>
<title>Create Todo</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
padding: 20px;
}
h1 {
color: #333;
}
form {
background: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 400px;
margin: auto;
}
label {
display: block;
margin-bottom: 10px;
font-weight: bold;
}
input, textarea {
width: 100%;
padding: 10px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
background-color: #5cb85c;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #4cae4c;
}
</style>
</head>
<body>
<h1>Add a New Todo</h1>
<form id="todoForm" method="POST" action="/todoCreate">
<label for="taskName">Task Name:</label>
<input type="text" id="taskName" name="taskName" required>
<label for="description">Description:</label>
<textarea id="description" name="description" rows="4" required></textarea>
<button type="submit">Add Todo</button>
</form>
</body>
</html>
`);
});
// MongoDB connection details
const BD_USER = process.env.BD_USER || "root";
const BD_PASSWORD = process.env.BD_PASSWORD || "example";
const BD_PORT = process.env.BD_PORT || "27017";
const BD_NAME = process.env.BD_NAME || "mydatabase";
const BD_HOST = process.env.BD_HOST || "mongodb"; // Service name from docker-compose
const URI = `mongodb://${BD_USER}:${BD_PASSWORD}@${BD_HOST}:${BD_PORT}/${BD_NAME}`;
// Connect to MongoDB
mongoose
.connect(URI)
.then(() => {
console.log("Connected to MongoDB");
})
.catch((err) => {
console.error("MongoDB connection error:", err);
});
// Define the Todo schema
const todoSchema = new mongoose.Schema({
taskName: { type: String, required: true },
description: { type: String, required: true },
});
const Todo = mongoose.model("Todo", todoSchema);
// API endpoint to create a new Todo item
app.post("/todoCreate", async (req, res) => {
const { taskName, description } = req.body;
const newTodo = new Todo({
taskName,
description,
});
try {
await newTodo.save();
res.send(`
<h1>Todo Created Successfully!</h1>
<a href="/">Add another</a>
`);
} catch (err) {
res.status(500).send(`
<h1>Error Adding Todo</h1>
<p>${err.message}</p>
<a href="/">Go back</a>
`);
}
});
// Run server
const PORT = process.env.PORT || 4002;
app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});
๐ Explanation
-
Dependencies: We import
express
,mongoose
, andpath
to set up the server and connect to MongoDB. -
Middleware:
-
express.json()
: Parses incoming JSON requests. -
express.urlencoded({ extended: true })
: Parses URL-encoded bodies (from form submissions). -
Routes:
-
GET /
: Serves an HTML form for creating new Todo items. -
POST /todoCreate
: Handles form submissions, creates a new Todo document in MongoDB, and responds with a success or error message. -
MongoDB Connection:
-
Connection URI is constructed using environment variables for flexibility.
- Mongoose connects to MongoDB using the provided URI.
-
Proper error handling ensures any connection issues are logged.
-
Todo Schema:
-
Defines the structure of Todo documents with
taskName
anddescription
fields. -
Server:
- Listens on port
4002
or the port specified in the environment variables.
๐ Connecting Docker Compose with the Application
Ensure that your Docker Compose configuration correctly links the node-app
service with the mongodb
service.
๐ docker-compose.yml Adjustments
Ensure the BD_HOST
in your Node.js application matches the service name of MongoDB in docker-compose.yml
, which is mongodb
.
Also, make sure that the node-app
service depends on the mongodb
service to ensure MongoDB starts first.
Updated src/index.js
Review the connection details in your src/index.js
to ensure they match the Docker Compose setup:
๐ฅ๏ธ Creating a Simple HTML Form
We've embedded the HTML form directly within the Express route (GET /
). However, you can enhance the UI using frameworks like Bootstrap for better styling. Here's how you can modify the form with Bootstrap:
Enhanced HTML Form with Bootstrap
Update the app.get("/")
route in src/index.js
:
app.get("/", (req, res) => {
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Todo</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h1 class="text-center">Add a New Todo</h1>
<form id="todoForm" method="POST" action="/todoCreate">
<div class="mb-3">
<label for="taskName" class="form-label">Task Name:</label>
<input type="text" class="form-control" id="taskName" name="taskName" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description:</label>
<textarea class="form-control" id="description" name="description" rows="4" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Add Todo</button>
</form>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
`);
});
Note: Including Bootstrap via CDN enhances the form's appearance without additional configuration.
๐ Running the Application
With all configurations in place, let's build and run the application using Docker Compose.
Step 1: Create the .env
File
Create a .env
file in the root directory to securely store environment variables:
Step 2: Build and Start Containers
Navigate to your project directory and run the following command to build and start the Docker containers:
Explanation:
--build
: Forces a rebuild of the Docker images.up
: Creates and starts the containers.- Services Started:
mongodb
: MongoDB database.node-app
: Node.js application.
Step 3: Accessing the Application
Once the containers are up and running:
-
Open the Form: Navigate to
http://localhost:4002/
in your web browser to access the Todo creation form. -
Add a Todo: Fill in the Task Name and Description fields and click Add Todo.
-
Confirmation: Upon successful submission, you should see a confirmation message. You can verify the addition by connecting to MongoDB and checking the
todos
collection.
๐งช Testing the Setup
To ensure everything is working correctly, perform the following tests:
Test 1: Adding a Todo
-
Access the Form: Go to
http://localhost:4002/
. -
Submit a Todo:
-
Task Name: Buy groceries
-
Description: Milk, Bread, Eggs
-
Confirm Addition: You should receive a success message indicating the Todo was created.
Test 2: Verifying in MongoDB
- Connect to MongoDB Shell:
If you're using Docker, you can access the MongoDB container's shell:
- Switch to Database:
- View Todos Collection:
You should see the Todo item you just added.
Test 3: Handling Errors
-
Submit an Incomplete Form: Try submitting the form without filling in the required fields to ensure validation works.
-
Incorrect Credentials: Modify the
.env
file with incorrect MongoDB credentials and observe if the application handles connection errors gracefully.
๐งน Cleaning Up
After testing, you might want to stop and remove the Docker containers to free up system resources.
Stop and Remove Containers
Note: This command stops and removes the containers but retains the data in the
mongo-data
volume.
Remove Volumes (Optional)
If you want to remove the persistent data as well:
Warning: This will delete all data stored in MongoDB.