Dockerize a Node.js + MongoDB App with Docker Compose
In modern software development, the classic 'it works on my machine' dilemma continues to plague engineering teams as differences in operating systems, Node.js runtimes, and local database versions cause deployment friction. **Docker** and **Docker Compose** eliminate this environment drift by packaging applications and their backing services into isolated, reproducible containers.
This comprehensive guide walks you through building a production-grade Node.js and Express REST API connected to a MongoDB database, containerizing the application using multi-stage Dockerfiles, and orchestrating local development and scaling using Docker Compose.
1. Understanding Containerized Multi-Service Architecture
When building containerized microservices, running an application and a database requires managing network communication and data persistence across multiple isolated containers.
- App Container (Node.js/Express): Runs the backend server code, handling client HTTP requests and interacting with the database driver.
- Database Container (MongoDB): Stores persistent collections and documents, utilizing Docker volumes to ensure data survives container restarts.
- Docker Compose: Acts as the multi-container orchestrator, defining both services, environment variables, internal bridge networks, and persistent storage volumes in a single `docker-compose.yml` file.
2. Setting Up the Node.js Express Application
Let's create a minimal Express server configured to connect to MongoDB using environment variables.
// server.js
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.json());
const PORT = process.env.PORT || 5000;
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/testdb';
mongoose.connect(MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB connected successfully'))
.catch(err => console.error('MongoDB connection error:', err));
app.get('/health', (req, res) => {
res.status(200).json({ status: 'UP', timestamp: new Date() });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
3. Writing a Production-Ready Multi-Stage Dockerfile
To keep production images lean and secure, we use a multi-stage build that separates development dependencies from production artifacts.
# Stage 1: Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Stage 2: Production runtime stage
FROM node:20-alpine AS runner
WORKDIR /app
# Create a non-privileged system user for security
RUN addgroup -g 1001 -S nodejs && adduser -u 1001 -S nodejs -G nodejs
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app ./
USER nodejs
EXPOSE 5000
CMD ["node", "server.js"]
4. Orchestrating Services with Docker Compose
Create a `docker-compose.yml` file in your project root to tie the Node.js application and MongoDB together.
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: node_app
restart: always
ports:
- "5000:5000"
environment:
- PORT=5000
- MONGO_URI=mongodb://mongo:27017/codenest
depends_on:
- mongo
networks:
- app-net
mongo:
image: mongo:7.0
container_name: mongodb
restart: always
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db
environment:
- MONGO_INITDB_DATABASE=codenest
networks:
- app-net
volumes:
mongo_data:
networks:
app-net:
driver: bridge
5. Running and Testing Your Containerized Stack
Spin up your complete development environment with a single terminal command and verify logs:
# Build and start containers in detached mode
docker compose up --d --build
# View container logs
docker compose logs -f
# Stop and clean up containers and networks
docker compose down
Conclusion
Containerizing a Node.js and MongoDB application with Docker Compose guarantees absolute environment consistency from local development to production servers. By leveraging multi-stage builds and persistent named volumes, engineering teams can achieve scalable, secure, and easily reproducible backend architectures.