Up and running with io.js and Docker

This is a quick post for getting up and running with io.js and a single, simple Dockerfile using Docker 1.6, which was just released. It is not an exhaustive post on using either io.js or Docker.

Install Docker

Go get Docker and install it from here.

Verify that Docker 1.6 is running:

$ docker -v
Docker version 1.6.0, build 4749651

Get the iojs image

Now go get the official iojs image:

$ docker pull iojs

This will download the latest iojs image from the official iojs repo on Docker Hub.

An image is used by Docker to launch a process in a container. An image is comprised of all the bits that provide an operating system environment for running a process. When the process runs, it runs in a "container" completely isolated from any other process as if it were running in its own machine.

You can verify that the image has been downloaded successfully by entering:

$ docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             VIRTUAL SIZE  
iojs                          latest              90f5055d3cce        9 days ago          700.6 MB  

You can verify that no containers are currently running by entering:

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES  

(Note that the column header names wrap, and so do any container entries, based on the width of your terminal)

If you have already been using Docker, you may see a few containers listed, but you shouldn't see any with the image name 'iojs' yet.

Run a container

The iojs image was built to run the iojs command if you don't explicitly specify a different command to run. Like node, the iojs command creates a REPL for entering JavaScript commands

You can test it like this:

$ docker run --rm -it iojs
>

The REPL just waits for you to enter JavaScript. You can enter <Ctrl-c> twice to exit the REPL.

What did we do? We told docker to start a container based on the iojs image. The default command for the iojs image is iojs, so it started a REPL and waited for JavaScript to be entered.

The reason it waited is because of the -it options. The i option keeps STDIN open for the process, and the t option allocates a pseudo-TTY. The combination is what allows us to interact with the REPL as if we had started it directly in a terminal instead of inside of a container.

When trying things out, you will create a lot of containers. It's a good idea to use the -rm option to automatically delete the container when you stop it to keep things tidy.

Override the default command

When we tell Docker to start a container from the iojs image, we can explicitly provide a command to override the default one. The following will run bash instead of

$ docker run --rm -it iojs bash
root@2ee47e2148ec:/#

This will put you at a bash prompt in the container. You can check the version of iojs you're running:

# iojs -v
v1.6.4

Since node is symlinked to iojs when iojs is installed, you can also just enter:

# node -v
v1.6.4

As you can see, the current iojs image runs iojs v.1.6.4.

The docker-iojs team tracks the latest io.js versions and updates the official iojs image for Docker as quickly as they can.

The io.js team releases new versions fairly rapidly and the newest version is now 1.7.1. The docker-iojs team supports three different iojs images (explained later). They move pretty quickly to update the iojs Dockerfiles (used to build images) with a pull request. They then submit yet another pull request with the information necessary to update the official repo and make the new images publicly available. The latest version (1.7.1) should be available within the next day or so.

Using the iojs:onbuild image

The iojs image we used above is not the only image the docker-iojs team maintains. They also maintain a 'slim' version (not recommended except under specific circumstances) and an 'onbuild' version that makes building derivative images easier.

The 'onbuild' version is based on the plain 'iojs' version used above, but it copies your node application to the container and then runs its. Creating a derivative of this image can be as simple as referencing it and specifying the port you want to expose.

Here's a simple example to illustrate. Create a demo directory.

Create a package.json file with the following:

{
  "name": "simple-docker-iojs-demo",
  "version": "1.0.0",
  "scripts": {
    "start": "node app.js"
  }
}

Create app.js and just print something to the console to prove it works:

console.log('hello world!');  

Create a Dockerfile:

FROM iojs:onbuild  

Now create an image:

$ docker build -t demo-app .
Sending build context to Docker daemon 4.096 kB  
Sending build context to Docker daemon  
Step 0 : FROM iojs:onbuild  
# Executing 3 build triggers
Trigger 0, COPY package.json /usr/src/app/  
Step 0 : COPY package.json /usr/src/app/  
 ---> Using cache
Trigger 1, RUN npm install  
Step 0 : RUN npm install  
 ---> Using cache
Trigger 2, COPY . /usr/src/app  
Step 0 : COPY . /usr/src/app  
 ---> Using cache
 ---> 9aa71d87d65d
Successfully built 9aa71d87d65d  

Docker will create an image based on the Dockerfile in the current directory and name it demo-app. As part of creating the image, it ran instructions from the 'onbuild' base image that we specified in our Dockerfile that included copying the contents of the current directory to /usr/src/app/ and running npm install.

You can see what the instructions look like here.

Now run demo-app in a container:

$ docker run --rm demo-app
npm info it worked if it ends with ok  
npm info using npm@2.7.5  
npm info using node@v1.6.4  
npm info prestart simple-docker-iojs-demo@1.0.0  
npm info start simple-docker-iojs-demo@1.0.0

> simple-docker-iojs-demo@1.0.0 start /usr/src/app
> node app.js

hello world!  
npm info poststart simple-docker-iojs-demo@1.0.0  
npm info ok  

The iojs app is trivial, but the mechanics of how this works is the same for more complicated examples. One thing you will probably want to do is export a port for accessing your node application, which we cover in the next section.

Pushing your image

If you are happy with your application at this point, you might want to push your image to Docker Hub. You will need to create an account on Docker Hub, then login from the command line:

$ docker login

Only official images (such as iojs) can have simple names. To push your own image, you will need to change the name of the image. Instead of demo-app, you will need to create the image using your login name. Mine is 'subfuzion' so my docker build and docker push commands would look like this:

$ docker build -t subfuzion/demo-app .
...
$ docker push subfuzion/demo-app
...

Exposing a port for a server

Modify the demo to make it an express app. Install express:

$ npm install --save express

Then edit app.js:

'use strict'  
const app = require('express')();  
const port = process.env.PORT || 3000;

app.use('/', function(req, res) {  
  res.json({ message: 'hello world' });
});

app.listen(port);  
console.log('listening on port ' + port);  

And update Dockerfile:

FROM iojs:onbuild  
expose 3000  

Rebuild the image:

$ docker build -t demo-app .

Now we can run it, but we want to map port 3000 inside the container to a port we can access from our system. We'll pick port 49100:

$ docker run --rm -p 49100:3000 demo-app

Now we can access the app via port 49100, which will be mapped to port 3000 in the container.

If you're using boot2docker on a Mac, the port is actually mapped to the Docker host virtual machine. To determine the IP address, enter this at the command line:

$ echo $(boot2docker ip)
192.168.59.103

You should be able to test http://192.168.59.103:49100/ in your browser.

The app looks for the environment variable PORT to be set, otherwise it defaults to 3000. We could specify an alternate port via the environment from the command line like this:

$ docker run --rm -p 49100:8080 -e "PORT=8080" demo-app

You will still access the app using the external port 49100, but it is now mapped to port 8080, which is what the app is listening to since the environment variable PORT was set.

Next time

We will cover the networking aspects in a bit more detail, show how you can mount the source instead of copying during development, and begin to cover the topic of orchestration, starting with a simple mongo dependency.

I originally published this article on Codefresh.