E2E Dockerizing a MEAN Stack Application
Introduction
Lately I've been getting familiar with Docker. I have built single container applications using a Dockerfile
and run them locally. This works fine, especially for deployment purposes to a production VM but it's no different than setting up the VM with all the required dependencies and pushing updates via FTP or source control. However, one of the features that I have found extremely useful is multi-container building and deployment via docker-compose
. With docker-compose
, not only can I build and run my application, but also dependent services like databases, caches, proxies, etc. Best of all, the builds are standardized and initialized at once without having to individually install the dependencies and components. This writeup explores how to containerize a MEAN stack application and set up a docker-compose.yml
file for it to build and start the server and database services defined within it.
Requirements
This writeup assumes that Docker
, Docker-Compose
and Node
are installed on your PC.
The Application
The application is a CRUD todo MEAN stack application. The repo for this application can be found here.
Project Structure
|_models (Mongoose models)
| |_todo.model.js
|_public
| |_scripts
| | |_controllers
| | | |_main.controller.js
| | |_services
| | |_todo.service.js
| |_views
| | |_main.html
| |_app.js (front-end application)
| |_index.html
|_Dockerfile (server service)
|_docker-compose.yml
|_api.js (todo api routes)
|_config.js
|_server.js (back-end application)
The front-end is built with AngularJS
and the back-end is built with NodeJS
using the Express
web framework and MongoDB
database. MongoDB
models are defined with the Mongoose
package. In the application, users can create, view, update and delete todo tasks. The Dockerfile
is used to define the container for the web application and the docker-compose.yml
defines both the MongoDB
database container as well as the web application container defined in the Dockerfile
.
The Docker File
#Define base image
FROM node:8
#Set Working Directory
WORKDIR /app
#Copy pakage.json file from current directory to working directory
ADD package.json /app
#Install npm packages
RUN npm install
#Copy all application files from local directory to working directory
ADD . /app
#Open port where app will be listening
EXPOSE 3000
#Start application
CMD ['npm','start']
Define Docker Image and Application Directory
The Dockerfile
has no extension and the syntax is like a standard text file. #
characters denote comments. Docker
works based off images which are basically pre-built packages that are stored in one of many registries such as DockerHub
. DockerHub
can be thought of as a package repository/manager like npm
or dpkg
. In the first two lines of the file we define which base image we want to create our container with. Since this is a MEAN stack application built entirely in JavaScript
, we'll be using the node
version 8 image. Then we want set the directory in which our application will reside. We do this by using the WORKDIR
command and setting /app
as our application directory, but any directory of your choosing is valid.
Install Dependecies
All of our dependencies should be defined in our package.json
file. In order to install these dependencies in our container, we need to copy our local package.json
file into our container application directory. This can be done with the ADD
command by passing the package.json
and application directory /app
as arguments. Once that file has been copied, it's time to install the dependencies. To run commands through the build process of the application we use the RUN
command. The command is no different than the one you'd use on your local machine. Therefore, to install the dependencies defined in the package.json
file we use the command RUN npm install
.
Copy Application Files
Once our dependencies are installed, we need to copy the rest of the files in our local project directory to our container application directory. Like with the package.json
file we use the ADD
command and pass .
and /app
as our source and destination arguments respectively.
.dockerignore
Something to keep in mind is that locally we have a node_modules
directory containing our installed dependencies. In the previous step, we ran the npm install
command which will create the node_modules
directory inside our container. Therefore, there is no need to copy all these files over. Like git
, we can set up a .dockerignore
file which will contain the files and directory to be ignored by Docker
when packaging and building the container. The .dockerignore
file looks like the following.
node_modules/*
Opening Ports
Our application will be listening for connections on a port. This particular application will use port 3000. We need to define the port to listen on in the Dockerfile
as well. To do so, we'll use the EXPOSE
command and pass the port(s) that the application will listen on. (MongoDB listens on 27017, but since the Dockerfile
only deals with the web application and not the database we only need to specify the web application's port).
Starting the Application
After our container is set up, dependencies are installed and port is defined, it's time to start our application. Unlike the process of running commands while building the container using the RUN
command, we'll use the CMD
command to start our application. The arguments accepted by this are an array of strings. In this case, we start our application like we would locally by typing in npm start
. The Dockerfile
command to start our application is the following CMD ['npm','start']
.
The docker-compose.yml File
version: '2'
services:
db:
image: mongo
ports:
- 27017:27017
web:
build: .
ports:
- 3000:3000
links:
- "db"
The docker-compose.yml
file is a way of defining, building and starting multi-container applications using docker-compose
. In our case we have a two container application, one of the containers is the web application we defined and built in the Dockerfile
and the other is a MongoDB
database. The docker-compose.yml
file can take many options, but the only ones we'll be using are the version
and services
option. The version
option defines which syntax version of the docker-compose.yml
file we'll be using. In our case we'll be using version 2. The services
option defines the individual containers to be packaged and initialized.
Services
As mentioned, we have two containers. The names of our containers are web
and db
. These names can be anything you want, as long as they're descriptive and make sense to you. The web
container will be our web application and the db
container will be our MongoDB
database. Notice that we have listed our db
service first and then our web
service. The reason for this is we want to build and initialize our database prior to our application so that by the time that the web application is initialized, it's able to successfully connect to the database. If done the other way around, an error will be thrown because the database will not be listening for connections and the web application won't be able to connect. Another way to ensure that our database is initialized prior to our web application is to use the links
option in our web
service and add the name of the database service db
to the list of dependent services. The ports
option like in our Dockerfile
defines which ports that container will need to operate. In this case, our web
app listens on port 3000 and the db
service will listen on port 27017.
Container Images
The docker-compose.yml
can build containers based on images hosted in a registry as well as those defined by a Dockerfile
. To use images from a registry, we use the image
option inside of our service. Take the db
service for example. MongoDB
already has an image in the DockerHub
registry which we will use to build the container. Our web
container does not have an image that is listed in a registry. However, we can still build an image based off a Dockerfile
. To do this, we use the build
option inside our service and pass the directory of the respective Dockerfile
containing the build instructions for the container.
Building and Running Containers
Now that our container definitions and files are set up, we're ready to build and run our application. This can be done by typing docker-compose up -d
in the terminal from inside our local project directory. The -d
option runs the command detached allowing us to continue using the terminal. This command will both build and start our containers simultaneously. Once the web
and db
containers are up and running, we can visit http://localhost:3000
from our browser to view and test our application. To stop the application, inside the local project directory, we can type docker-compose stop
in our terminal to stop both containers.
Conclusion
This writeup uses a pre-configured MEAN stack CRUD todo application and explores how to define a single container Dockerfile
as well as a multi-container application using docker-compose
. Docker streamlines how applications are built and deployed while docker-compose
allows more complex multi-container applications to be orchestrated, linked, deployed and managed simultaneously allowing developers to spend more time developing solutions and less time managing infrastructure and dependencies.
Links/Resources
Docker Community Edition
Docker: Getting Started
NodeJS