Dockerize your MEAN application

Last week I wanted to make my MEAN application (MEAN standing for MongoDB / ExpressJS / AngularJS / NodeJS) able to run on Docker containers. As I use a very common Yeoman generator as a bootstrap for my app and MEAN is now a very common configuration, I thought I’ll find some very complete tutorials on the Internet. After some research I find some kick starters on how to Dockerize a Node app, how to Dockerize a Mongo database but nothing truly helpful on how to Dockerize both in the same app using the ExpressJS configuration files.

So this post is a quick “how to” for a zero to a fully Dockerize MEAN application using 2 containers:

  • 1 container for the NodeJS part that will be called -app container,
  • 1 container for the MongoDB part that will be called -mongo container.

The 2 containers will be linked together using Docker user-defined network links. For commodity, they will be launched and stopped using Docker Compose (the tool for defining and running multi-container Docker applications).

 

Directories and Files

Before diving deep into Docker stuffs, here’s a brief description of the directory structure I use. This is a typical structure initialized by Yeoman generator “angular-fullstack”. myapp is here a placeholder for your own app’s name, /client is where all frontend and AngularJS code stands, /server is for NodeJS / ExpressJS controllers, models and components, /e2e holds your end-to-end tests and /node_modules your NodeJS dependencies.

/myapp
  /client
  /dist
  /e2e
  /node_modules
  /server
  ...

A specific directory here is /dist: that’s where all your minified, prepared and “ready for production” code goes when you (or your CI Bot ;-)) have run your build tools command such as grunt build or gulp build. In this post, we assume that we want to Dockerize only this “ready for production” code, thus that /dist directory exists and has been provisionned by your build process.

 

Docker build file for your Node app

First step here is to Dockerize your application so that it can be run using a simple docker run command. For that, we’ll start from a base Docker image named node:argon. Maybe Argon is not the most lightweight image out there for NodeJS app, but it contains all the dependencies you need for building native extensions. So it’s generally a good choice to start with Argon.

We’ll add Docker build directives for our app into a file called Dockerfile. Because we’re are going to have other files and I like to separate stuffs, I suggest you to create this file into a new /docker directory under root. Complete relative path for file is /myapp/docker/Dockerfile.

Here is its content:

FROM node:argon

MAINTAINER Firstname Lastname <myemail@mycorp.com>

# Define working directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# Copy files and install dependencies
COPY /dist/package.json /usr/src/app
RUN npm install --production
COPY dist /usr/src/app

# Set the running environment as production
ENV NODE_ENV production

# Expose on specified network port
EXPOSE 8080

# Executing defaults
CMD ["node", "server/app.js"]

Just notice the ENV directive that is necessary for ExpressJS bootstrap phase to select the correct configuration for connecting to MongoDB.

So now, you just have to build you application image. Be sure to have your terminale being at your application root directory before running the following command (replace mycorp and myapp placeholder ;-))

docker build -f docker/Dockerfile -t mycorp/myapp-app .

To speed up the image creation process a little bit, I’ll suggest you adding a .dockerignore file into your root directory. This tells Docker to ignore directories when creating the build context for your image. After all, only the /dist directory is important for us. Here is the content of the /myapp/.dockerignore file.

client
e2e
node_modules
server

Having build your image, you can now launch your first application container running:

docker run --name myapp-app -p 8080:8080 -d mycorp/myapp-app

Unfortunately, container fails after few seconds because there’s no MongoDB database to connect to… You can inspect the reason of the failure by executing docker logs myapp-app.

 

ExpressJS MongoDB configuration

Before launching another container for our database, we need to prepare some configuration files… As you may know, ExpressJS allows you to hold different configurations for different environments. As our focus is on production environment, the file we need to edit is /myapp/server/config/environment/production.js. In this file, we’re going to add a new line within the mongo.uri configuration property. This property is already defined using the major MongoDB Cloud providers environment variables, we’re going to add our own one as highlighted below.

// MongoDB connection options
mongo: {
  uri:    process.env.MONGOLAB_URI ||
          process.env.MONGOHQ_URL ||
          process.env.OPENSHIFT_MONGODB_DB_URL+process.env.OPENSHIFT_APP_NAME ||
          process.env.MONGODOCKER_URI ||
          'mongodb://localhost/smartpilotapp'
}

The goal here is to keep the default behaviour of ExpressJS bootstrap: if MongoDB URI is not defined as a environment variable, we’ll fall back to a local instance. Do not forget re-building your application Docker image after this step! (grunt build and then docker build ...).

 

Docker Compose configuration

So we’ve talked about a MongoDB dedicated container on introduction… Fortunately, we do not have to build ourself this container: it exists on the shelf here on Docker Hub. Because, my app have been started for months now, it uses the 2.6 version of Mongo but it should be ok on version 3.0 too.

Next thing we have to do is now to describe what are the dependencies and the links between the containers composing our application. This is done using a docker-compose.yml file that yo can also store into the /myapp/docker directory. The content of this file is just below:

version: '2'
services:
  mongo:
    image: mongo:2.6.12
    container_name: myapp-mongo
    volumes:
      - "~/tmp/myapp-data:/data/db"
  app:
    depends_on:
      - mongo
    image: mycorp/myapp-app:latest
    container_name: myapp-app
    ports:
      - "8080:8080"
    environment:
      - MONGODOCKER_URI=mongodb://mongo:27017/myapp-app
    links:
      - mongo:mongo

You can notice here:

  • the declaration of 2 services and associated container names on lines 5 and 12,
  • the dependency declaration on line 10 so that -mong container will be started before -app container,
  • the declaration of a persistent volume associated to -mongo container on line 7. This directory must be created onto your Docker host before launching the containers ; so that documents persisted by MongoDB will survive a container failure and restart,
  • the link definition on line 18 with the formservice:alias so that the mongo service will be seen as mongo from within -app container,
  • our MongoDB URI environment variable assignment on line 16 that makes ExpressJS find a valid URI during its bootstrap process.

 

Running everything!

Now that all the above steps are completed, we simply have to use Docker commands to run our application.

From the /myapp/docker directory, within a terminal, just use docker-compose:

laurent@ponyo:~/dev/myapp/docker$ docker-compose up -d
Creating myapp-mongo
Creating myapp-app
laurent@ponyo:~/dev/myapp/docker$

Check everything is running with docker ps:

laurent@ponyo:~/dev/myapp/docker$ docker-compose up -d
CONTAINER ID        IMAGE                              COMMAND                  CREATED             STATUS              PORTS                    NAMES
56cc36936c7e        mycorp/myapp-app:latest            "node server/app.js"     6 seconds ago       Up 5 seconds        0.0.0.0:8080->8080/tcp   myapp-app
bb13813d5841        mongo:2.6.12                       "/entrypoint.sh mongo"   6 seconds ago       Up 6 seconds        27017/tcp                myapp-mongo
laurent@ponyo:~/dev/myapp/docker$

And finally, stop:

laurent@ponyo:~/dev/myapp/docker$ docker-compose stop
Stopping myapp-app ... done
Stopping myapp-mongo ... done
laurent@ponyo:~/dev/myapp/docker$

and clean-up everything:

laurent@ponyo:~/dev/myapp/docker$ docker-compose rm
Going to remove myapp-app, myapp-mongo
Are you sure? [yN] y
Removing myapp-app ... done
Removing myapp-mongo ... done
laurent@ponyo:~/dev/myapp/docker$

I hope this post helped you get up and running a MEAN application on Docker. Do not hesitate commenting!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s