Custom Development

Docker for Devs: Containerize Your Application Development

Max McCarty

This post was also published on LockMeDown.com, a security-focused blog for developers written by Max McCarty.

This is the first post in a series called Docker for Devs, which will include:

As developers, we’re always looking for a shortcut, or an easier way to hit the ground running, right? If you’re a team lead, getting your team on the same page, setup and operating with minimal effort and pain is important. Docker can help.

In the realm of software development, there has always been a growing emphasis on modularization, from general principles such as single-responsibility to more concrete implementations such modularizing javascript functionality into stateless components. Here, I’ll show you how we can use Docker to modularize our development environment for a number of similar benefits, including helping  us hit the ground running.

Docker 4 Developers

As you know, to get ramped up on a project, you have a checklist of to-dos:

  • Pull the code base from repository
  • Install external tools such as database(s), caching store, additional tools and services
  • Patch and update said external tools
  • Configure databases and services for cross-application communication
  • Cross fingers and pray (times may vary)
  • Debug (reboot at least 3x)

Imagine if after pulling your application’s code base out of repository, you only had to run a few command lines (possibly as few as one) to get your entire application’s environment ready to go. Sounds cool, right?

That’s exactly what we're out to accomplish. Instead of an encyclopedic approach to laying out all the features and commands of using Docker, I’ll cover the main features as we go through using Docker to containerize a developer’s environment.

This post is the first in a series on leveraging the power of containerization using Docker to easily build an applications development environment that can be shared and up and running in no time.  

Docker Toolbox vs. Docker for X

Docker Toolbox was the original collection of tools available for working with a number of Docker resources and will vary depending on your OS of choice. But since then, they have released new Windows and Mac native applications.  So in addition to the Linux, OS X and Windows Docker Toolbox variations, you will also see “Docker for Mac” and “Docker for Windows”.

To understand and decide which tool you should use, I wanted to outline the premise for the new native applications.  The original Docker Toolbox would set up a number of tools along with the use of VirtualBox. It would also provision a virtual machine running on the Linux Hypervisor for either Windows or Mac.

VirtualBox, Hyper-V and hyperkit, O'My

The native applications, such as in the case of Docker for Mac, install an actual native OS X application.  It also no longer uses VirtualBox but the OS X hypervisor hyperkit. Furthermore, shared interfaces and network is managed much simpler.  There are also some user experience updates to the tooling used to work with Docker as well.  These same changes are also apparent in the Docker for Windows native application, utilizing the Hyper-V hypervisor instead, along with the host of other similar network and tooling updates.

In the end, the experience is suppose to be a more positive, efficient experience as well as less error-prone.  However, you will find a vast majority of external documentation related to the Toolbox - so it could be to your advantage to know about Toolbox.

Getting Started

This initial tutorial will simply use an out-of-the-box express.js application. When we get to live editing of source code and communicating between containers, I’ll move onto a more involved, universal React.js application running Webpack’s dev-server with hot module reloading, a MongoDB database and more.

 

Step 1: Installation

In an effort to get to the goodies of using Docker and containerizing a developer environment along with a slew of various OS versions, I’ll leave it to you to download whichever Docker version (Toolbox or native) and OS you want to use:

Step 1a: Download Docker for (OS) or Docker Toolbox

Toolbox: https://www.docker.com/products/docker-toolbox (OS X and Windows)

Docker for Windows: https://www.docker.com/products/docker#/windows

Docker for Mac: https://www.docker.com/products/docker#/mac

TOOLBOX USERS: Docker Toolbox comes with a “Docker Quickstart Terminal” and is linked to the Docker environment when ran.  However, it’s common to run a terminal separately OR within your IDE of choice.  In order to interact with Docker from another terminal/prompt, you’ll need to initialize the Docker env by running “docker-machine env”.  At the end of the displayed text is a command you will need to copy/paste from within that same terminal/prompt to initialize the Docker environment.

Step 1b: Installation and Docker Daemon

Instead of rewriting the same instructions that Docker has already provided installing either Toolbox or Docker for Mac/Windows, use the following links if you need help getting it installed.  In addition, each instructions will provide information about the Docker Daemon that runs in the background and grants you access to Docker preferences and other features.

Toolbox Installation: https://docs.docker.com/toolbox/overview/

Docker for Mac Installation: https://docs.docker.com/docker-for-mac/

Docker for Windows Installation: https://docs.docker.com/docker-for-windows/

 

Step 2: Source Code / Environment

The next step is to have obtain the source code of the development application you want to containerize.  

For now, you can grab this project from Github that I’m using in this part of the tutorial to save a few steps.

 

Step 3: Creating a Docker Image File

Remember, the primary goal is to run our application in an isolated and modularized environment. In order to have Docker create that environment, we have to tell it how to create it. We do that with a set of instructions using a Docker Image File.  

IMPORTANT: This Docker image file will allow us to create an image which will represent that containerized environment we’ve been talking about, and eventually create running instances of that image called docker containers. But we’re jumping ahead, more on this in Step 5.

  1. Using your IDE of choice, add a file and name it "Dockerfile" to the project root:
  2. Copy in the below file contents

Dockerfile

FROM mhart/alpine-node:6.9.2

WORKDIR /var/app

COPY . /var/app

RUN npm install --production

EXPOSE 3000

ENV NODE_ENV=production

CMD ["node", "bin/www”]

What did we do?

Dockerfile is the default name that Docker looks for in an image file, but it can be any name, which we’ll see in a later step. These instructions inform Docker that we want to create an image:

  • FROM a base image mhart/alpine-node with the tag of 6.9.2.
  • Setting the working directory WORKDIR that the application will run from
  • COPY the contents of the current local directory “." to the specified location
  • Then RUN the command “npm install —production” from the WORKDIR
  • We’ll also EXPOSE the port of 3000 from the container when it is created from the image
  • In addition, we wanted to create a container local ENV variable of NODE_ENV set to “production
  • Finally, we want to start our express.js server, which resides in the “www” file in the “bin” directory execute the command “node bin/www"

TIP: Docker always requires a base image, keeping our images with as small of a footprint as possible is key.  The alpine-node image is built on the extremely small (49.65mb) that includes both Node.js and NPM.

 

Step 4: Building an Image

Now that we have an Docker image file, which specifies details about how to create an image, let's stop and talk about what an Image is:

 

Image-File-Layer.png

An image is a read-only layered file system.  It makes up the base file system of the environment we keep eluding to. The Docker image file we made in the previous step will instruct Docker on how to create each of these layers for the image we are going to create from that Dockerfile now.

But if it is a “read-only” file system, how are we going to write to it, (for instance, performing code changes to our application's code-base during development)?  Good question, I’ll answer that in an upcoming step.

Step 4a: build production image
  1. From a terminal/prompt navigate to the root of our project directory.
  2. Run the command (including the period at the end) :

    docker build -t express-prod-i .

image-build.gif

TIP: For Docker Toolbox, running Docker commands from anywhere but the Docker Quickstart Terminal will result in an error (varies based on OS). In order to run docker commands from any random terminal/prompt, you’ll need to link up the terminal/prompt to the docker environment.  Run docker-machine env, and run the command that it prompts such as e.g. "@FOR /f "tokens=*" %i IN ('docker-machine env') DO @%i"

What did we do?

  1. We ran the Docker build to create an image from the Docker image file we create in Step 3.
  2. Using the tag switch -t we gave the image a name that we can use for referring to it without having to refer to the Docker generated ID (e.g. 50e8dde7e180)
  3. We specified the path of the image which was the current local directory which was specified with the “.”
Step 4b: Verify image

If all went as planned we should see (as in the .gif) a “successful build ….” message.  We can now run a command you’ll come to memorize for showing your images:

  1. docker images

docker-images-list.png

In this case, our combined image is 61.26 MB in size which is a combined of the base alpine-node image and our express application layer.

Step 4c: review image file layers

If we want to see the file layers created for our image we can run the command:

  1. docker history express-prod-i

image-layer-history.png

 

Step 5: Running an Instance of our Image

Now that we have created an image, we are ready to create an isolated, modularized environment of our application. As I have alluded to a number of times, that environment is a Docker container.  A docker container is actually a running instance of an image.

In concept alone, an image and container is very similar to how we would think of a Javascript Prototype or a OOP Class.  

So let’s run an instance of the image we created

Step 5A: Running an instance of our image
  1. Run the command:
    docker run -d --name express-prod-app -p 7000:3000 express-prod-i

  2. Launch browser to http://localhost:7000

running-express-app.png

What did we do?

We created and ran an instance of our express-prod-i image known as a container by:

  • Using the Docker RUN command
  • Specifying the detached mode -d flag (so we don’t tie up the current terminal/prompt)
  • and gave a name of “express-prod-app" to our container using the --name flag
  • along with mapping a local port on our host machine of 7000 to the internal container port that we exposed of 3000 using the -p flag
  • Finally, specifying the “express-prod-i" image we want to generate and run our instance

NOTE: I used a different local port of 7000 to show you that you can map whatever unused port on the host machine to the internal port being exposed in the container.

TIP: Without the -d we would have started the running container in an attached mode. This means it would feedall output from the container directly into the terminal/prompt we executed the command in.  By starting the container in a detached mode, we can continue to work from the terminal/prompt that we ran the container in.

Epiphany #1

Did you catch that?  We didn’t have to install Node.js or NPM locally.  We didn’t have to run npm install to prime the application locally, and we didn’t run the application from our host machine.  All these things are happening within the host machine, and this is a simple express.js demo site.

Step 5b: verify running containers

We can see running containers using the command:

  • docker ps

container-list.png

This will show us running containers.  We can also specify the -a flag to see ALL containers. It can be useful when things go wrong and we want to see if a supposedly running container has exited unexpectedly:

  • docker ps -a

docker-all-containers-list.png
Unrelated example of ps -a command

 

Step 6: Stopping and Starting Containers

Stopping a running container is easy as running the…you guessed it:

  • docker stop express-prod-app

And starting:

  • docker start express-prod-app

 

Step 7: Deleting Images and Containers

As we go through these tutorials, it could come in hand to drop an image or container and start over. Below are the steps to do so.  In addition, further down under the Bonus section, there are a few helpful shortcuts:

Step 7a: Deleting Containers

To delete a container, we use the remove container "rm” command:  

  • docker rm express-prod-app

To get the container back, we can use the command we issued before:

  • docker run -d --name express-prod-app -p 7000:3000 express-prod-i
Step 7b: Deleting Images

To delete an image, we will use the remove image “rmi” command:  

  • docker rmi express-prod-i

To get the image back, we can use the command we issued before:

  • docker build -t express-prod-i .

TIP: The remove image and container commands can take the Docker image or container ID and even just the first few characters of the ID.  So you have the option to specify either.  (You’ll also see that we do so in the Bonus section.)

 

BONUS:

In addition to the steps to delete images and containers, you can also utilize a few helpful shortcuts that have come in handy for me.

Delete All Containers

To delete all the containers, run the following remove “rm” command, but execute the command to return all of the containers within that command:

Windows Users: You will need a bash shell to run the following commands.  Mainly because of the GNU make variable references $( ) we use.  You can install something like “Bash on Ubuntu on Windows” or even GIT bash.  Just remember that a you will need to link up your terminal by running docker-machine env and running the eval command that is listed at the end.

  • docker rm $(docker ps -a -q)

What did we do?

  1. We issued the container remove command “rm
  2. But instead of providing the tag or container ID we provided a list of the docker container ID by
  3. Providing the ALL (-a) flag along with the QUIET (-q) which only returns numeric Docker ID’s.

Delete All Images

To delete all the images we can do something similar and use the remove images command “rmi”.

  1. docker rmi $(docker images -q)

What did we do?

  1. Here we used the remove image command but instead gave it a list of docker image ID’s
  2. by using the list docker images command
  3. and specifying the QUIET (-q) flag which only returns numeric Docker ID's.

 

Production? I Thought We Were Building a Container for Application Development?

It doesn’t take a genius to notice that the image we just created we were notifying Express.js and Node.js that we want to run in production. So what’s the deal?  

Remember how we need to have a base image for any Docker image?  And who wants to have to ‘rebuild’ an image every time we make a change in development to our application?  So we are going to use our production image as a base image for our development image and isolate the development changes to the layered image specified for development.

 

Ok, My Head’s Spinning

So, I’ve already dropped a bunch of knowledge about docker.  Creating a docker image file, building an image and generating a running container of that image.  But we have a lot to go.  

So in the next post in this series, I’ll tackle creating an image that will be our development version or our application, but based off our production image.  We’ll also look at how we can leverage scripts in our container to help set up the local environment for making changes to our source code.



Max McCarty
ABOUT THE AUTHOR

Max McCarty is a Senior Technical Consultant at Summa with a passion for breathing life into big ideas. He is the founder and owner of LockMeDown.com and host of the popular Lock Me Down podcast. As a software engineer, Max’s focus is on software security, and strongly believes in empowering the everyday developer with the information to write more secure software. When he’s not building new applications or writing about web security, you’ll find Max burning calories with his kids and spending time with his wonderful family. He’s also a serious history buff.