Last week I gave a quick Docker demo to some colleagues at our internal developers hour to give them an overview of what it is and what it can do. In this blog post I run through a complete example showing how to take a Node JS application, and create a Docker image from it.
In the forthcoming part 2, I will show how to extend this and use Docker Compose to effectively get a host of related containers up and running very quickly.
If you’d like to follow along with the tutorial and don’t currently have a Node JS application that you want to dockerize, you can check out the repo on Github. The add-docker branch contains the application with all the Docker bits added to it, if you want to skip ahead to the final ‘dockerized’ app.
The vast majority of this tutorial will be in the command line – so get comfortable!
Docker is many things to many people. If you’re unfamiliar with Docker, think of it as a way to take your application, along with the environment in which it runs – its runtime, data, configuration and so on – and package it into a nice little box that you can pass around or duplicate, and have it run exactly the same way wherever it goes.
It’s also a way that you can get up and running with various development environments very quickly, without having to install everything that you need onto your host system. Need to create a node application but don’t want to install nvm? Just download the node Docker image and get it to build your code on your host system.
Finally, you can also easily consume applications without bothering with all the setup. Need a MongoDB instance or MySql server? Just download the relevant image and you’re pretty much ready to go. The install time for these systems and applications simply becomes the time that it takes for you to download the image and execute it.
By default, these images come from the public Docker hub. You are able to create a free repository too and host your own images, which we shall get into in a later tutorial.
Note: As an aside, Docker used to require a Linux environment in which to execute, meaning that on Windows and Mac OSX they had to resort to a virtual machine environment to get it to work on those platforms. Luckily, native tools are now available for Windows and OSX, meaning that all that unnecessary complication has gone away.
For now, head over to https://www.docker.com/products/docker and download the version of Docker for your operating system. Once it has installed, you will see a little Whale icon in your status bar. You can also run the following command in the terminal to verify that it is installed and available:
$ docker -v
Mine is reported as
Docker version 1.12.0, build 8eab29e, experimental; yours may vary slightly.
You might also want to install Node JS, if you haven’t already Â (do a
$ node -v in the terminal to check). The application that I’m using for this tutorial was developed using NodeJS 6.3.1, but I would think that anything above 4.0 would work just as well. The app uses some of the more modern ES6 features, so anything below 4.0 probably won’t compile.
However, you only need to install NodeJS if you wish to run the application locally before creating a Docker image for it. Otherwise, wait until you can run the container to see it in action!
The demo application
If you’re dockerizing your own Node application, you can skip this section and continue to the next.
Grab the demo app by cloning into a directory on your hard drive:
$ git clone email@example.com:elkdanger/blog-docker-node
or if you’re using HTTPS:
$ git clone https://github.com/elkdanger/blog-docker-node.git
Change directory into the app directory. If you have node installed and want to test the app out, install the node packages:
$ npm install
The run the application:
$ node index.js
Then, when you browse to http://localhost:3000, it should write out the text string “Hello, world!” to the browser window. Pretty simple!
Press Ctrl+C to stop the application once you’re done.
Creating the Dockerfile
When you build a Docker image from an application, you generally use a Dockerfile. This is simply a file which instructs Docker how to build the image, including which image to base your application on, what ports to expose, what commands to run inside the container, and what to do when your container is started.
To expand a little on the terminology, there are two major components at play here – images, and containers. Images contain the definition of your application and its environment plus any initial data that it needs, whereas Containers are the running instances of your application, and are in factÂ created from an image.
Inside the application directory, create a new blank file named ‘Dockerfile’. Let’s have a look at the one I created for my demo application. A lot of this will be self-explanatory but we’ll go through it regardless.
Starting at the top,
FROM node:6.3.1-slim tells Docker which image we want to base our image on. If you imagine that a Docker image is actually a series of layers all stacked on top of each other – we just want to layer our application on top. This
node:6.3.1-slim image is a set of layers that effectively contains a base OS layer, then a layer with Node v6.3.1 installed through apt-get, then some more layers that set up environment variables and other things. Then we’re going to stick on another layer containing our application code. If you really want to, you can start with a base linux image and install Node yourself through apt-get; starting with a predefined node image just makes this process easier for you.
Note: Looking at the image name
node:6.3.1-slim, you’ll notice that it includes the Node version number. This is known as the tag. Handily, vendors tend to tag reusable apps and platform services with its version number, allowing you to run different version of the same app locally. If you want to test your app against different versions of a service or application, it’s just a case of downloading the image with the right tag and they will happily run side-by-side.
Next we’ll create a directory to hold our application code.
RUN mkdir -p /usr/src/app simply executes a normal shell command to create a new folder at /usr/src/app inside the container. Executing a RUN command also adds a new ‘layer’ to our image.
WORKDIR /usr/src/app tells Docker that this directory should be our working directory when a container is created from this image, and for when any commands are executed during the build process.
Next, we are going to add our package.json file to the image, and in the next step we do
RUN npm install. This is kind of an optimisation which is special to Node apps. To explain; Docker has a nice feature whereby it can cache layers that have been built previously, and reuse them if the files concerned haven’t changed. In our case, we can skip subsequent node package installs if our package.json file doesn’t change, reusing the cached layer and making our builds a lot faster.
Next, we copy the rest of our application files using
COPY . /usr/src/app. This would normally also include the node_modules folder, which we don’t really want to copy. In a minute I will show you a way around this using a
EXPOSE 3000 tells Docker that we want to expose port 3000 from the container, so that something from the outside can connect to it. Think of this as simply opening a port; we still have to map it to a port on the host when we run a container.
Finally, we tell Docker what command it should execute when the container is started. In our case, we want node to execute our entry JS file and start the web server. We do this using
CMD ["node", "index.js"].
Note: For some discussion on why the CMD command is expressed this way, refer to the Docker docs
This is the complete file, and contains everything Docker should need in order to build your image. To build your image, drop back to the command line and execute the
build command from inside the application directory, where your Dockerfile is:
$ docker build -t mynodeimage .
This will read the Dockerfile in the current directory and create an image from it called
mynodeimage. The first time you execute this, Docker will download the base Node image (which is a good couple of hundred megs) and then execute the rest of your commands in sequence. It will be much faster on subsequent builds since it won’t have to download the base image again, unless you delete it or change the FROM command.
You can verify that your image has been created by using the
images command, which displays all the images Docker currently knows about:
$ docker images
The result will look something like this, showing your image and also the node image that you’ve based it on.
When performing the COPY instruction, it’s likely that there are certain files and folders that you do not wish to distribute with your app, such as temporary build files or environment variable files. In our case, we wish to ignore the node_modules folder, as this will be generated for us by the
npm install command that is run by the container.
To ignore the folder when the app is copied, create a new file along side your Dockerfile called
.dockerignore with these contents:
Running your application using Docker
To create a running instance of you app, orÂ container, we use the
$ docker run -it -p 3000:3000 --name node_app mynodeimage
Here we run the image, mapping port 3000 to the same port on the host and telling it that we want to run it interactively (-it), which means that the container will be attached to our current console session. You can instead run the container in the background as a daemon, using the -d switch. We also specify a name for our container using the –name switch. You can leave this out, in which case Docker will generate a random name for your container.
This does now mean that your application is up and running again, this time from a Docker container! Open a browser and browse to http://localhost:3000 once again to verify that the application is working. To stop the container you can use Ctrl+C, or execute
$ docker stop <container id or name> if you ran it as a daemon. To find out what its id or name is, you can execute the
ps command, which shows you a list of all the containers currently running:
$ docker ps
You can quite happily start and stop this image using the
$ docker start node_app and
$ docker stop node_app commands respectively. Note that, if you want to run your app again from the same image, you will have to give it a different name for the container or remove the existing one. To do that, you can use
$ docker rm -f node_app to stop and remove the existing container, before creating a new one.
Over the next couple of blog posts, I’ll go into more detail about cool Docker things, such as:
- Modifying running containers, committing changes and pushing images to the Docker hub
- More detailed and customised Docker files