How to create a Docker + Node.js + MongoDB + Varnish environment
I started learning Docker.io over the weekend and I must say that it's really cool, unfortunatelly, this is still a new project and you don't find lots of documentation and tutorials online. At first, I struggled a little to do what I'm gonna show you, but here's the whole explanation so you don't have to. We're going to create a simple blog application written in Node.js running on a MongoDB database with a Varnish Load Balancer (covered on a different post) on top of the Node.js instances. We're going to use Docker and I strongly recommend to use Vagrant too, so you don't mess around too much with your host. In this tutorial, I won't cover installing Docker or the basics, since there's enough of that out there.
First we need to create a new directory, in this directory I'll download 3 Git projects:
- A simple Blog application made with Node.js and Express.js MVC
- The Docker Node.js image
- The Docker MongoDB image
After you download everything, you can take a look at the Dockerfiles, I'll explain a little what are they doing and how to run them.
Node.js Docker image
This one has a Dockerfile, a README, a run.sh file and a start.sh file. The start.sh file is intended to be used inside the container so you won’t really be using it but it’s important that you don’t modify it unless you know what you’re doing.
The run.sh file is there so you can type ‘sh run.sh’ instead of the whole docker command which can get really long.
The Dockerfile will install nodejs, use npm to install expressjsmvc, express, bower and nodemon; and then it will expose the port 3000 before adding the ‘start.sh’ file to the container and then run it.
Now, I must explain something that I struggled with a little bit. When you’re using Dockerfiles (by now you should know what are they) you basically build your image so you can run containers and this container will run as specified. What this basically means is that you can create a container that will run a command as soon as it’s created or you can make this commands optional.
This is a huge difference and it really depends on the service you’re configuring. When you use the ENTRYPOINT property in your Dockerfile basically you’re telling the image to run that command as soon as the container is created, so if you do something like this:
Dockerfile:
ENTRYPOINT ["/start.sh"]
It means that the container will run the ‘start.sh’ file when it’s created. If you later want to do something like:
$ docker run -it luis/nodejs bash
To access the container, it will not work, since the container will just ignore everything and will run ‘start.sh’
So in order to have both, sometimes you’ll need to do CMD instead of ENTRYPOINT, this way, the container will run the command if you don’t pass any commands, but if you do pass any commands, it will run them.
If I replace my Dockerfile with
CMD ["/start.sh"]
Then
$ docker run -it luis/nodejs bash
Will run bash, and:
$ docker run -it luis/nodejs
Will run start.sh
MongoDB Docker image
FROM ubuntu:12.04
MAINTAINER Luis Elizondo, lelizondo@gmail.com
RUN apt-get update
################## BEGIN INSTALLATION ####################### Install MongoDB Following the Instructions at MongoDB Docs
Ref: http://docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/
Add the package verification key
RUN apt-key adv –keyserver hkp://keyserver.ubuntu.com:80 –recv 7F0CEB10
Add MongoDB to the repository sources list
RUN echo ‘deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen’ | tee /etc/apt/sources.list.d/mongodb.list
Update the repository sources list once more
RUN apt-get update
Install MongoDB package (.deb)
RUN apt-get install -y mongodb-10gen
Create the default data directory
RUN mkdir -p /data/db
##################### INSTALLATION END #####################
Expose the default port
EXPOSE 27017
Default port to execute the entrypoint (MongoDB)
CMD [”–port 27017”]
Set default container command
ENTRYPOINT /usr/bin/mongod </code>
This one is easier, it will expose two ports and then run the mongod service using ENTRYPOINT, so you cannot really access the container unless you rewrite the entrypoint.
Build the images
The next step is to build your images, go to each directory containing a docker image and run:
$ docker build -t myname/image-name .
That command will build the image and tag it with a name, these are the commands I used:
$ docker build -t luis/nodejs .
$ docker build -t luis/mongodb .
If I want to list all my images, I just do:
$ docker images
REPOSITORY TAG IMAGE ID
luis/nodejs latest 3e9589892ef9
luis/mongodb latest 79868a4506c7
</code>
How to link my docker containers?
By now, you should be able to run your containers really easy and they should work, but they’re not linked, we need the Node container to access the MongoDB container. The first thing we need to do is to start a MongoDB container.
$ docker run -itd -p 27017 --name mongodb luis/mongodb
When we run this command, docker will create a new container with the image “luis/mongodb” and name that container as “mongodb”, it will also link the port 27017 to whatever port the container exposes, and it will also run this container as a daemon. This is very important since we want the container to start and keep running.
Wait, what about files?
Both MongoDB and your application need to read/write data on the HD, and you probably want to persist that data outside the container, which is disposable. The solution is to link volumes. First, let’s do this for MongoDB.
By default, MongoDB, inside the container, will save the data in /data/db, and that’s fine, we actually created that directory inside the container on the Dockerfile. MongoDB will think is saving the data in /data/db but in reality, it will be saving the data outside the container in a directory we specify.
Let’s create a new folder to save the data outside the container.
$ sudo mkdir -p /var/mongodb
And now, let’s fool MongoDB to save the data in /var/mongodb
$ docker run -itd -p 27017 -v /var/mongodb:/data/db --name mongodb luis/mongodb
What we’re doing differently is to link /var/mongodb (outside the container) to /data/db (inside the container).
The same principle will apply to your application files.
Linking containers
Now we can go back to linking our containers. First, make sure you clone the application, in my case, I’m using a Node.js application that I created earlier, my files are at /home/luis/Docker/blog-example
Now, to run my node.js container linked to MongoDB all I have to do is:
$ docker run -itd -p 8000:3000 --name nodejs --link mongodb:mongodb -v /home/luis/Docker/blog-example:/var/www luis/nodejs
Let’s explain what we’re doing with that command. First, we know my container will expose the port 3000, so we’re redirecting that port to the port 8000 (eventually we’ll have to modify this but for now we’re OK). Second, we set a name for the container. Third, we link the mongodb container, which basically allows the Node.js container to access the MongoDB container. Finally, we set the real path for /var/www, fooling our container for the real location of our files.
My application is not working
The application needs to install some stuff before running, so we’re going to need to install the dependencies before we run it.
Let’s kill the container first:
$ docker rm -f nodejs
And now, let’s bash into it:
$ docker run -it -p 8000:3000 --link mongodb:mongodb -v /home/luis/Docker/blog-example:/var/www luis/nodejs bash
Notice that we’re not daemonizing the container so we can actually access it.
If we list all the files in /var/www we’ll see that our application is there:
$ ls -la /var/www
drwxr-xr-x 7 1000 1000 4096 Mar 25 05:09 .
drwxr-xr-x 20 root root 4096 Mar 25 05:20 ..
-rw-r--r-- 1 1000 1000 34 Mar 25 05:09 .bowerrc
drwxr-xr-x 8 1000 1000 4096 Mar 25 05:09 .git
-rw-r--r-- 1 1000 1000 44 Mar 25 05:09 .gitignore
-rw-r--r-- 1 1000 1000 1377 Mar 25 05:09 app.js
-rw-r--r-- 1 1000 1000 260 Mar 25 05:09 bower.json
drwxr-xr-x 4 1000 1000 4096 Mar 25 05:09 components
-rw-r--r-- 1 1000 1000 169 Mar 25 05:09 expressjsmvc.json
drwxr-xr-x 2 1000 1000 4096 Mar 25 05:09 lib
-rw-r--r-- 1 1000 1000 327 Mar 25 05:09 package.json
drwxr-xr-x 4 1000 1000 4096 Mar 25 05:09 public
-rw-r--r-- 1 1000 1000 824 Mar 25 05:09 start.js
drwxr-xr-x 2 1000 1000 4096 Mar 25 05:09 views
Environment variables
Before I install everything, I want to show you a cool thing call environment variables. These are variables that are accessible by your system, do list them just do:
$ env
HOSTNAME=7921e0543e40
MONGODB_NAME=/focused_engelbart/mongodb
MONGODB_PORT_27017_TCP=tcp://172.17.0.2:27017
TERM=xterm
MONGODB_PORT=tcp://172.17.0.2:27017
MONGODB_PORT_27017_TCP_PORT=27017
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
MONGODB_PORT_27017_TCP_PROTO=tcp
SHLVL=1
HOME=/
MONGODB_PORT_27017_TCP_ADDR=172.17.0.2
_=/usr/bin/env
As you can see, I have several variables that reference MongoDB, this is because of the link we created. If we take a look at our application we can see that we’re using those variables.
var address = process.env.MONGODB_PORT_27017_TCP_ADDR;
var port = process.env.MONGODB_PORT_27017_TCP_PORT;
mongoose.connect("mongodb://" + address + ":" + port + "/blog");
Now let’s install all dependencies:
$ cd /var/www
$ npm install ; expressjsmvc install
Now, let’s just exit our container:
$ exit
And let’s run it again:
$ docker run -itd -p 8000:3000 --name nodejs --link mongodb:mongodb -v /home/luis/Docker/blog-example:/var/www luis/nodejs
Now, let’s see what’s going on inside the container with:
$ docker logs nodejs
And finally, let’s open the browser and go to http://localhost:8000
You should see out application up and running. You can add a new blog post going to http://localhost:8000/blogs/add
What about Varnish?
This blog post already got too long, so that’s going to have to wait until the next post.