Building Simple Multi-Container Application with Docker

Roman Ceresnak, PhD
CodeX
Published in
8 min readFeb 17, 2023

A simple tutorial on how to containerize an application consisting of a frontend, a backend, and a database.

Without further ado, let’s get down to business. First, we will containerize the non-relational MongoDB database.

Dockerize the MongoDB Service

At this point, I’d like to start from scratch and create a container for a non-relational MongoDB database. As the frontend backend runs outside the container (yet), it needs to open communication with the container running on port 27017. The MongoDB server in the image listens on the standard MongoDB port, 27017, so connecting via Docker networks will be the same as connecting to a remote mongod.

Therefore I run the command:

  • docker run — — name mongodb — — rm -d -p 27017:27017 mongo
  • mongodb will be the name of the container
  • — — rm will remove the stopped container
  • — detach or –d flag in the docker run command, means that a Docker container runs in the background of your terminal
  • -p is for port mapping that is used to access the services running inside a Docker container. We open a host port to give us access to a corresponding open port inside the Docker container. Then all the requests that are made to the host port can be redirected into the Docker container.
  • mongo is the official image that can be found on the official website(https://hub.docker.com/)

Dockerize The Node Server App

Since I wrote code that does not exist in the docker registry image, it is necessary to create a file in the backend project called Dockerfile that will contain the following instructions:

  • FROM node
  • WORKDIR /app
  • COPY package.json
  • RUN npm install
  • COPY . .
  • EXPOSE 80
  • CMD [“node”, “server.js”]

Unfortunately, I had already created many images and started getting a little lost in them. That’s why I wanted to clean up and delete images that are no longer needed and especially not used.

  • docker image prune -a

Now create an image based on the created Dockerfile.

  • create docker image => docker build -t backend-app .
  • create a container => docker run — — name backend-con — — rm backend-app

In this case, we created a container for the backend server. Communication between containers is blocked by default. We need to adjust the database connection settings in the server part. Up to this moment, the server connected to the MongoDB guest, but it is already running in containers, so we have to modify the connection to the database.

Old connection:

mongoose.connect("mongodb://localhost:27017/collectionName", {
useNewUrlParser: true,
useUnifiedTopology: true
});

Updated connection:

mongoose.connect("mongodb://host.docker.internal:27017/collectionName", {
useNewUrlParser: true,
useUnifiedTopology: true
});

We have to recreate the image because we made changes in the source code:

  • recreate docker image => docker build -t backend-app .
  • recreate a container => docker run — — name backend-con — — rm backend-app

At this moment, the connection of the backend part with the database will work for us, but the connection of the frontend with the backend will not work for us. This is because we containerized the server part and did not make the backend part accessible to the outside world. Let’s fix it right away.

  • docker run — — name backend-con — — rm -d -p 80:80 backend-app
  • 80 is the internal port, where the application is listening
  • 80 is the localhost port

Decorize the Angular SPA into a Container

Since I wrote code that does not exist in the docker registry image, it is necessary to create a file in the frontend project called Dockerfile that will contain the following instructions:

  • FROM node
  • WORKDIR /app
  • COPY package.json
  • RUN npm install
  • COPY . .
  • EXPOSE 3000
  • CMD [“npm”, “start”]

Based on created Dockerfile file let’s create an image and container:

  • create image => docker build -t frontend-app
  • create container => docker run — — name frontend-con — — rm -d -p 3000: 3000 — —frontend-app

In this stage, the frontend application is able to communicate with the backend application and the backend application with the database.

The problem with this solution is that the containers communicate via localhost, which is a satisfactory solution if we don’t want to move our storage anywhere else. However, if we want to deploy the solution on any of the clouds, i.e., AWS, Azure, or Google Cloud, then the current solution is not sufficient. Therefore, let’s update the solution together.

Running Containers

In this stage, stop all the containers, and let’s go update communication between containers.

  • docker stop frontend-con
  • docker stop backend-con
  • docker stop mongodb

Let’s add Docker Network for Efficient Cross-Container Communication

  • Let’s create a network: docker network create medium-net
  • Let’s add the Mongo database to the network: docker run — — name mongodb — — rm -d — —network medium-net mongo

We have to update our backend code because we have to reference the backend server to the MongoDB instance in the newly created container.

Old connection

mongoose.connect("mongodb://host.docker.internal:27017/collectionName", {
useNewUrlParser: true,
useUnifiedTopology: true
});

New Connection:

mongoose.connect("mongodb://mongodb:27017/collectionName", {
useNewUrlParser: true,
useUnifiedTopology: true
});

Update the code, with only the connection string (mongodb) pointing to the MongoDB container name. If you intend to add or use a different name, the connection string must match the given container name.

  • Rebuild the image => docker build -t backend-app .
  • Rebuild container => docker run — — name backend-con — — rm -d — — network medium-net -p 80:80 backend-app

Let’s dockerize the frontend application.

  • rebuild image => docker build -t frontend-app
  • rebuild container => docker run — — name frontend-con — —rm -d -p 3000:3000 -it frontend-app

With this step, we created a system where the containers will be able to communicate with each other, but the solution has a big problem. What if the database crashes? What if we change the code? Will the container automatically restart?

Adding Data Data persistence to MongoDB with Volumes and Authentication Protection

MongoDB stores data inside the container in the folder :/data/db and therefore we have to create a named volume:

  • docker run — — name mongodb -v data:/data/db — — rm — d — — network medium-net mongo

At this time anyone can store data and retrieve data as well. So let’s secure the connection to the container. Let’s add some authentication methods.

  • Stop the container => docker stop mongodb
  • Create new a container => docker run — — name mongodb -v data:/data/db — — rm — d — —network medium-net -e MONGO_INITDB_ROOT_USERNAME=roman -e MONGO_INITDB_ROOT_PASSWORD=secret mongo

At this moment, the database is working correctly, but the problem occurs with the backend. We added additional authentication, so we have to adjust the connection to the database.

Old connection:

mongoose.connect("mongodb://mongodb:27017/collectionName", {
useNewUrlParser: true,
useUnifiedTopology: true
});

New connection:

mongoose.connect("mongodb://roman:secret@mongodb:27017/collectionName?authSource=admin", {
useNewUrlParser: true,
useUnifiedTopology: true
});

Because we made changes in the source code we have to rebuild the image and recreate the container:

  • rebuild an image => docker build -t backend-app .
  • recreate a container => docker run — — name backend-con — — rm — d — — network medium-net -p 80:80 backend-app

Modernize backend part

In the following part we will focus on 3 parts:

  • Data persistence

For storing data in a container we need to define volumes. So the first step is to stop the running container for the backend app.

  • docker stop backend-app

And let’s add volumes for the container:

  • docker run — — name backend-con -v C:\Users\romanceresnak\Downloads\Docker\medium\backend/:app -v logs:/app/logs -v /app/node_modules -d — — rm -p 80:80 — — network medium-net backend-app

At this moment we are ensuring that we will not lose data in case of restarting the container.

  • Live Source Code Update

In package.json we have to make some changes. The nodemon is a mechanism that is more than useful.

Nodemon is a popular tool that is used for the development of applications based on node.js. It simply restarts the node application whenever it observes the changes in the file present in the working directory of your project.

Additionally, nodemon does not need any specific modifications to code or the mode of development. It acts as a facilitator in the node by replacing the wrapper for it. To use nodemon, you will simply need to replace the word node on the CLI while you are about to execute your script.

In package.json add the following code:

"devDependencies": {
"nodemon": "^2.0.4"
}

And after that change the start script:

"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon server.js"
}

Nodemon is setup correctly but we have to update the Dockerfile content.

  • FROM node
  • WORKDIR /app
  • COPY package.json .
  • RUN npm install
  • COPY . .
  • EXPOSE 80
  • CMD [“node”, “server.js”]

Because we made changes in the Dockerfile we have to stop the container, rebuild the image and after that rebuild the container:

  • stop container => docker stop backend-con
  • rebuild image => docker build -t backend-app .
  • recreate container => docker run — — name backend-con -v C:\Users\romanceresnak\Downloads\Docker\medium\backend/:app -v logs:/app/logs -v /app/node_modules -d — — rm -p 80:80 — — network medium-net backend-app

At this moment we are ensuring that we will not lose data in case of restarting the container. The situation when the source code of the backend application changes, this situation is also taken care of because the nodemon automatically takes care of it.

  • Hard-coded MongoDB password and user name

The main problem is with the hard-coded database name and password and therefore I updated the Dockerfile following way:

  • FROM node
  • WORKDIR /app
  • COPY package.json .
  • RUN npm install
  • COPY . .
  • EXPOSE 80
  • ENV MONGODB_USERNAME=root
  • ENV MONGODB_PASSWORD=secret
  • CMD [“node”, “app.js”]

I have added two new environment variables to which we have to refer. I updated the connection to MongoDB the following way.

Old Connection

mongoose.connect("mongodb://roman:secret@mongodb:27017/collectionName?authSource=admin", {
useNewUrlParser: true,
useUnifiedTopology: true
});

New Connection

mongoose.connect("mongodb://${process.env.MONGODB_USERNAME}:{process.env.MONGODB_PASSWORD}@mongodb:27017/collectionName?authSource=admin", {
useNewUrlParser: true,
useUnifiedTopology: true
});
  • After that, we have to stop the container => docker stop backend-app
  • Rebuild the container => docker run — — name backend-con -v C:\Users\romanceresnak\Downloads\Docker\medium\backend/:app -v logs:/app/logs -v /app/node_modules -e MONGODB_USERNAME=roman -d — — rm -p 80:80 — — network medium-net backend-app

Update frontend application

We need to add volumes to the container

  • docker run — — name frontend-con -v C:\Users\romanceresnak\Downloads\Docker\medium\frontend/:app — — rm -p 3000:3000 -it frontend-app

Conclusion

Docker technology offers various development possibilities and sometimes it is really difficult to know the aspects of the given technology. I hope you liked the article. Bye bye.

Updated approach can be seen in my next article:

--

--

Roman Ceresnak, PhD
CodeX
Writer for

AWS Cloud Architect. I write about education, fitness and programming. My website is pickupcloud.io