Connect Node to MongoDB with Docker and Docker Compose
Learn how to connect a Node server to a MongoDB database using Docker and Docker Compose. We will also learn about creation scripts, how to handle node_modules with Docker, and more.
Table of Contents 📖
- Environment Variable Setup
- Creating the Node Server
- Creating the Node Dockerfile
- Creating the Mongo Dockerfile
- Creating Volumes with Docker Compose
- node_modules Issues with Docker
- Creating Node Service with Docker Compose
- Creating Mongo Service with Docker Compose
- Running the Program
Environment Variable Setup
To begin, lets set up our environment variables. These will be used by Docker Compose to set the location of our Mongo server, Node server, and also carry out some initial setup. First, lets add the database variables.
DATABASE_HOST=mongo-c
DATABASE_PORT=27017
DATABASE_COLLECTION=user
These variables contain the port and location of our Mongo docker container in the docker network. We also create a collection called user. Now lets add some environment variables that the Mongo Docker image reserves for initialization.
MONGO_INITDB_ROOT_USERNAME=WittCode
MONGO_INITDB_ROOT_PASSWORD=MySecurePassword
MONGO_INITDB_DATABASE=mydb
The names of these variables are very important as they are reserved by the MongoDB docker image to add authentication and set a default database. Now lets add our server variables.
SERVER_HOST=server-c
SERVER_PORT=7005
These set the location and port of our Node docker container in the docker network.
Creating the Node Server
Now lets write the code for our Node server. First, lets initialize an empty directory as an ES6 npm project.
npm init es6 -y
We will also install nodemon as a development dependency to get live code updates.
npm i nodemon -D
Now, inside our package.json file, lets create a start script.
"scripts": {
"start": "nodemon ."
},
Now lets set the main file of our project to be server.js inside our src directory.
"main": "src/server.js"
Now, to connect a Node application to MongoDB, we need a MongoDB driver. A MongoDB driver is essentially software that manages connecting to MongoDB. We can install it from npm.
npm i mongodb
Now that we have our driver installed, lets import our packages and instantiate some environment variables.
import {MongoClient} from 'mongodb';
import http from 'http';
const DATABASE_USERNAME = process.env.MONGO_INITDB_ROOT_USERNAME;
const DATABASE_PASSWORD = process.env.MONGO_INITDB_ROOT_PASSWORD;
const DATABASE_DB = process.env.MONGO_INITDB_DATABASE;
const DATABASE_HOST = process.env.DATABASE_HOST;
const DATABASE_PORT = process.env.DATABASE_PORT;
const DATABASE_COLLECTION = process.env.DATABASE_COLLECTION;
const SERVER_HOST = process.env.SERVER_HOST;
const SERVER_PORT = process.env.SERVER_PORT;
- Import a MongoClient. A MongoClient is an object that connects to our Mongo server.
- Import the http module. We will use the http module to create a server to respond to requests.
- Set the environment variables. These will be used by Docker Compose to set the location of our Mongo server, Node server, and also carry out some initial setup.
Next, lets write the code to connect our MongoClient to the Mongo server.
const URI = `mongodb://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}`;
const client = new MongoClient(URI);
const db = client.db(DATABASE_DB);
const collection = db.collection(DATABASE_COLLECTION);
- Create a connection URI. A connection URI is a URI that contains the connection information to connect a MongoClient to a Mongo server. Its syntax consists of a protocol, authentication information, a host, and a port.
- Pass the connection URI to the MongoClient constructor.
- Find the database and collection that we will use.
Now lets create a main function to run our application.
async function main() {
try {
// Connect to Mongo
await client.connect();
console.log('Connected to Mongo!');
// Create server and listen for requests
const server = http.createServer();
// Query Mongo
server.on('request', async (req, res) => {
const result = await collection.findOne();
res.end(JSON.stringify(result));
});
server.listen(SERVER_PORT, SERVER_HOST);
} catch (err) {
console.error('Something went wrong', err);
}
}
main()
.then(() => console.log('Server started!'))
.catch(err => console.error('Something went wrong', err));
This function connects to Mongo, creates a server, and listens for requests. When a request is received it queries the collection and sends back the response.
Creating the Node Dockerfile
Now lets create the Dockerfile to build our Node image.
FROM node:20-alpine
WORKDIR /server
COPY package*.json .
RUN npm i
CMD ["npm", "start"]
- Use Node version 20 as the base image.
- Set the working directory to /server. This means any RUN, CMD, ENTRYPOINT, COPY, and ADD commands will be executed in this directory.
- Copy over package.json and package-lock.json.
- Install dependencies from npm.
- Start the Node server.
Creating the Mongo Dockerfile
Now lets create the Dockerfile to build the Mongo image. For this demonstration, we will use Mongo version 7.0.7 as the base image.
FROM mongo:7.0.7
Now lets make a creation script to add data to our default database that Docker will create using the MONGO_INITDB_DATABASE environment variable. Creation scripts are .sh or .js files placed inside the folder docker-entrypoint-initdb.d in the image. They are used for database initialization such as authentication, collection creation, data insertion, etc.
COPY init.js /docker-entrypoint-initdb.d/
Here, we are placing an initialization script called init.js inside this folder. Lets create this creation script and make it insert an entity into a collection called user.
db.createCollection('user');
db.user.insertOne(
{
username: 'WittCepter',
password: 'The best chrome extension',
email: 'a@a.com',
subscribedAt: new Date()
}
);
The db object here is the Mongo database object used inside the MongoDB shell. We will see that in action in a bit.
Creating Volumes with Docker Compose
Now lets start working with Docker Compose. To begin, we will create the required volumes.
volumes:
server-v-node-modules:
name: "server-v-node-modules"
database-v:
name: "database-v"
The top-level volumes declaration lets us configure named volumes. The name property sets a custom name for the volume. Running the command docker compose up for the first time will create these volumes. The volumes will be reused when the command is ran subsequently. The server-v-node-modules volume will handle our node_modules folder and the database-v volume will persist our MongoDB data.
node_modules Issues with Docker
The node_modules folder can be problematic for Docker if it contains packages with binaries specific to certain operating systems. In other words, certain packages will install different files depending on the operating system of the computer. This can cause issues if you are devloping an application with Docker as the Docker container doesn't always use the same OS as the host computer. This is why we create a volume called server-v-node-modules to handle our node_modules.
Creating Node Service with Docker Compose
Now lets use Docker Compose to create our Node service.
version: "3.9"
services:
server:
image: server:1.0.0
container_name: ${SERVER_HOST}
build:
context: ./server
dockerfile: Dockerfile
env_file: .env
ports:
- ${SERVER_PORT}:${SERVER_PORT}
volumes:
- ./server:/server
- server-v-node-modules:/server/node_modules
depends_on:
- database
- Create a service called server. This will be our Node server.
- Name the image server:1.0.0. If both the image and build declarations are specified, the image declaration specifies the name and tag of the Docker image.
- The container_name declaration specifies a container name as opposed to a generated default name.
- The build declaration contains options applied at build time. The context declaration is the path to a directory containing a Dockerfile or a URL to a Git repository. The dockerfile property is the name of the Dockerfile to use.
- The env_file declaration loads environment variables from a .env file into the image.
- The ports declaration maps a container port to a host port.
- The volumes declaration defines named volumes used by the container. The host volume will bring all our server code into the container.
- The depends_on declaration makes it so the node service will wait until our database service has started.
Creating Mongo Service with Docker Compose
Now lets use Docker Compose to create our Mongo service.
database:
image: database:1.0.0
container_name: ${DATABASE_HOST}
build:
context: ./database
dockerfile: Dockerfile
env_file: .env
ports:
- ${DATABASE_PORT}:${DATABASE_PORT}
volumes:
- database-v:/data/db
- Create a service called database.
- Set the image name to database and give it the version 1.0.0.
- Name the container from the environment variable DATABASE_HOST.
- Use the build property to specify the path to the Mongo Dockerfile.
- Load environment variables from the .env file into the image.
- Map a container port to a host port.
- Setup the database-v named volume to persist the data stored in Mongo. To persist the data stored inside a Mongo image, we need to create a volume pointing to the /data/db directory inside the image.
Running the Program
To run the program, all we need to do is run the command docker compose up at the top level of our project and specify the location of our environment variable file with the flag --env-file.
docker compose --env-file .env up
server-c | Connected to Mongo!
server-c | Server started!
In the console we should see our server output as well as lots of Mongo logs. To double check that everything is working, lets send a curl to our server.
curl localhost:7005
{"_id":"66080f19ce70e7b006a4b8b7","username":"WittCepter","password":"The best chrome extension","email":"a@a.com","subscribedAt":"2024-03-30T13:09:45.665Z"}
What we get back is the user information that we inserted into MongoDB with our creation script. It was the Node server that retrieved this information from Mongo.