In previous posts, we explored Docker basics, container networking, and AI integrations. If you haven't checked them out yet, visit my blog on Dev.to.
In this blog, we'll combine our Docker learnings to build a more sensible, real-world application using multiple containers. A typical full-stack application is composed of three essential building blocks:
Database ⇄ Backend ⇄ Frontend
We'll walk through building a multi-container setup for a course goals application using MongoDB, Node.js, and React.
Prerequisites
- Docker installed and running
- Basic understanding of Node.js, React, and MongoDB
- Familiarity with Dockerfile, volumes, and networks
Step 1: Dockerizing MongoDB (Database)
MongoDB offers a ready-to-use Docker image. Start a MongoDB container with:
docker run --name mongodb --rm -d -p 27017:27017 mongo
Explanation:
-
-p 27017:27017
: Maps the MongoDB port to localhost -
-d
: Runs in detached mode -
--rm
: Automatically removes the container when stopped
However, if you try to connect from a containerized backend using:
mongoose.connect('mongodb://localhost:27017/course-goals')
It will fail, because localhost
in a container refers to the container itself. To solve this, change it to:
mongoose.connect('mongodb://host.docker.internal:27017/course-goals')
This enables access to the host system from within a Docker container.
Step 2: Dockerizing the Backend (Node.js + Express)
Navigate to the backend/
folder and create a Dockerfile
:
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 80
CMD ["node", "app.js"]
Build and run the backend container:
docker build -t goals-node .
docker run --name goals-backend --rm -p 80:80 -d goals-node
Step 3: Dockerizing the Frontend (React)
Navigate to the frontend/
folder and create a Dockerfile
:
FROM node
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Build and run the frontend:
docker build -t goals-react .
docker run --name goals-frontend --rm -d -it -p 3000:3000 goals-react
Now, your app is accessible via http://localhost:3000
.
Step 4: Creating a Docker Network
Let’s create a Docker network so the containers can communicate internally:
docker network create goals-net
Re-run MongoDB inside this network:
docker run --name mongodb --rm -d --network goals-net mongo
Update backend connection:
mongoose.connect('mongodb://mongodb:27017/course-goals')
The container name mongodb
acts like a hostname here.
Rebuild and run the backend:
docker build -t goals-node .
docker run --name goals-backend --rm -d -p 80:80 --network goals-net goals-node
Note: Frontend (browser-based) apps cannot use container names to make HTTP requests. Hence, exposing backend on the host (localhost) using -p 80:80
is still required.
Step 5: Adding Data Persistence to MongoDB
Without persistence, MongoDB data is lost when the container stops. Create a named volume:
docker run --name mongodb -v data:/data/db --rm -d --network goals-net mongo
Now the data survives restarts.
Step 6: Enabling Authentication in MongoDB
Mongo supports setting root user credentials using environment variables:
docker run --name mongodb -v data:/data/db --rm -d --network goals-net \
-e MONGO_INITDB_ROOT_USERNAME=Mayank \
-e MONGO_INITDB_ROOT_PASSWORD=Gupta \
mongo
Update the backend connection string:
mongoose.connect(
'mongodb://Mayank:Gupta@mongodb:27017/course-goals?authSource=admin'
)
Step 7: Live Source Code Updates with Bind Mounts
Bind mounts are useful in development to reflect live code changes without rebuilding.
Example backend run command:
docker run --name goals-backend \
-v $(pwd):/app \
-v logs:/app/logs \
-v /app/node_modules \
--rm -p 80:80 \
--network goals-net \
goals-node
Frontend with live code updates:
docker run -v $(pwd)/frontend/src:/app/src \
--name goals-frontend --rm -p 3000:3000 -it goals-react
Step 8: Using Environment Variables
Avoid hardcoding credentials by using ENV variables.
Update Dockerfile:
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 80
ENV MONGODB_USERNAME=Mayank
ENV MONGODB_PASSWORD=Gupta
CMD ["node", "app.js"]
Update connection string in code:
const uri = `mongodb://${process.env.MONGODB_USERNAME}:${process.env.MONGODB_PASSWORD}@mongodb:27017/course-goals?authSource=admin`
mongoose.connect(uri)
Run backend container:
docker run --name goals-backend \
-v $(pwd):/app \
-v logs:/app/logs \
-v /app/node_modules \
--rm -p 80:80 \
-e MONGODB_USERNAME=Mayank \
-e MONGODB_PASSWORD=Gupta \
--network goals-net \
goals-node
Step 9: Optimizing with .dockerignore
Create a .dockerignore
file in both frontend and backend directories:
node_modules
.git
Dockerfile
This reduces the build context and speeds up Docker builds.
Conclusion
You now have a production-ready multi-container application using Docker for:
- MongoDB (database)
- Node.js (backend API)
- React (frontend UI)
We leveraged Docker volumes, environment variables, bind mounts, authentication, and inter-container networking — all essential for real-world applications.
This practice not only prepares you for scalable deployments but also mirrors the infrastructure used in many modern microservices architectures.