Dockerizing a MEAN-stack application - 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.