Dockerizing a MEAN-stack application - Part 2

The Node/Express server has already been placed inside a container, and now we'll need to connect it to the database. In this post, I'll describe how I added a MongoDB container to the server and how they can communicate with each other. Part 2.

If you haven’t read Part 1, it might be a good idea to at least skim through it, because I’ll refer to the files and folders in this post.

The source code of the project can be downloaded from my GitHub page.

Add database

Although I have the Docker image set up for the server, it will throw an error if I start running a container off of it, because server.ts tries connecting to the database.

I chose to use MongoDB as it can be seen from the source code.

So I went to Docker Hub and searched for the official Mongo image to get some instructions on how to set up the container.

I used the official mongo image, so I didn’t create any dockerfile for the database. Instead, I set up a docker-compose.yml file to start both the server and the database with just one command.

docker-compose is used for development, but again, this application will never reach real production and will never have real users. My goal here was to simulate a possible stack of the production environment.

docker-compose

I created a docker-compose.yml file in the root folder in which I added both the Node/Express server and the database.

At this point, it looked something like this:

version: '3'

services:
  db:
    image: mongo:latest
    container_name: mongo-movie-app-container
    volumes:
      - mongo-data:/data/db
    networks:
      - api-network
  node-api:
    build:
      context: .
      args:
        - mongodb_container
        - app_env
    image: node-express-movie-api
    container_name: node-movie-api-container
    env_file: .env
    networks:
      - api-network
    depends_on:
      - db

volumes:
  mongo-data:

networks:
  api-network:

The db service

I initially created two services.

The db service is for the MongoDB database. The image is the latest official Mongo image (mongo:latest, available from Docker Hub). I was happy to use the image created by the guys at MongoDB, they probably know what they are doing.

I like logging container messages, so I decided to give names to the containers. Docker will name by default, but I prefer using my custom names. In this case, terminal logs related to this container will be displayed as mongo-movie-app-container (container_name).

The next part is important. I created a named volume called mongo-data, where the saved movies are stored and fetched from.

The volumes section inside the db service has the named volume (mongo-data) first, this refers to the host (my computer). The second segment of the volume definition is the path to the database inside the MongoDB container (/data/db), separated by a colon (:).

The named volume also needs to be declared in a separate volumes section at the services level.

This way the data will be persistent. If I run the app now and save some movies to the database, and then stop the containers and run them again tomorrow, the saved movies will be available. This is really cool, because I don’t have to install MongoDB on my computer, and I can still use the database.

One more thing left from the db service, and this is the networks. I wanted the database and the server containers to communicate with each other, so I set up a common network (api-network).

Containers connected to the same network open up their ports to each other. This way I don’t have to declare the port 27017 in my docker-compose.yml and the database won’t be available from outside, only through my server. I can call the route endpoints directly, and Docker will know how to forward the request from the server container to the database.

Similarly to volumes, networks needs to be declared at the services level.

The node-api service

The structure for the server container is slightly different from the database. I created a separate dockerfile (Dockerfile, see Part 1) for this image and some extra configurations are needed in docker-compose.yml.

The build configuration contains two definitions. context: . refers to Dockerfile, which is in the same folder as docker-compose.yml, hence the . in the configuration. It means that the image will be built based on Dockerfile, which is the default file name for a dockerfile. I also displayed args as an array, these are the build-time arguments I refer to in Dockerfile (see Part 1).

image and container_name define the name of the image and container, respectively.

A few words on .env

I have an env_file definition here, which refers to a .env file created in the same (root) folder. The .env file contains all environment variables that are necessary to run the application.

It looks like this:

# .env in the root folder
MONGO_URI=mongodb://mongo-movie-app-container:27017/db-production
PORT=80
API_KEY=XXXXXXXX

When docker-compose starts the containers, it will find the environment variable definitions in the .env file, and makes them available for the server inside the Docker container.

server.ts refers to the MONGO_URI and PORT variables. MONGO_URI is used by Mongoose to set up the connection and PORT is used by app.listen to set up the server port.

API_KEY is the open movie database API key, with which I can fetch data from the database.

Two important things here.

First, MONGO_URI contains the name of the MongoDB container. This is the exact same name I defined in docker-compose.yml for the database container.

So there’s no localhost:27017 or 127.0.0.1:27017.

With this, Docker will know which container to connect to, and this will obviously be the container called mongo-movie-app-container. The connection to the database still occurs on port 27017, and Docker will manage this by itself.

mongo-movie-app-container and db-production are the values for the mongodb_container and app_env build time arguments from Dockerfile. Please refer to Part 1, I’ll show the dockerfile I created for the server there.

Second, PORT is equal to 80. This is because the only port exposed to the world will be the default port, which is 80. This will be managed by the client-side container and the nginx server (coming in Part 3).

Back to node-api

The node-api service will also be connected to the api-network, as it was discussed above.

The depends_on configuration means that the server container depends on the database. The database should start first and then the server, otherwise we’ll get a MongoDB connection error.

Start the app

The backend of the application is now ready and can be started with the docker-compose up command. We need to navigate to the root folder in the terminal, where docker-compose.yml is located.

Once the app has started, the endpoints can be tested with Postman or curl. We are now able to perform the CRUD-operations on the Mongo database.

Conclusion

This is the end of Part 2 of the dockerizing the movie app series. One piece, the client side, is still missing from the puzzle, and this will be covered in the last part.

Thanks for reading and see you next time.