Docker Series - Part 2

Creating and using Dockerfile to create custom images

Published: 01 April 2020

docker containers devops dockerfile docker hub images 

DockerFile


Dockerfile is a way to write instructions for docker so it can perform the tasks as needed.

Think of it as using Terminal on Mac/Linux except you write the instructions once and group them together in a file for Docker to run in the future.

There are many 2 basic parts to any dockerfile:

  • pre-defined name/type of instruction (comes from Docker)
  • actual command to execute
# DockerFile

# any image can be used as long as it fulfills our system requirements
FROM alpine

RUN apk add --update redis

CMD ["redis-server"]

In the above example, FROM, RUN & CMD are the pre-defined names/types of instructions that Docker recognises. Each of these are followed by the actual commands to execute.

  • FROM --> used to fetch a base Image from Docker Hub
  • RUN --> running command with that base Image
  • CMD --> attaching a command to the container for when it boots up

We can now execute this dockerfile inside a directory like so:

docker build .

NOTE: The . in the above command represents the build context or the location of the directory where Dockerfile lives. So, if it lives in the base of a directory/project, and we are executing the above command from the base of the directory, we can simply run the above command as is. Otherwise, we will need to either switch directories or specify the correct path.

This will give us an ImageID which we can use to start a container.

docker run <ImageID>

Build Process In-depth


Let's use the above dockerfile example to explain what goes on behind the scenes:

  • Step 1 - Docker pulls the alpine image from DockerHub and creates a container from it. Then takes a picture of that container. Let's call it Container-Image-111 for reference. It then destroys the container.
  • Step 2 - It creates a new container using Container-Image-111 and downloads Redis. Again, takes a picture of this updated container. Lets call it Container-Image-222 which overrides Container-111 image and then destroys this container as well.
  • Step 3 - Finally, it creates a new container using Container-Image-222, attaches a startup command to it, takes a new picture Container-Image-333 which overrides Container-Image-222. Then destroys container and returns the imageID.

An interesting feature of Docker is its ability to cache everything. So, if we run the above build command again without making any changes to the Dockerfile, it will be lightening quick because Docker will realise it has already run these steps before and so will serve everything from its memory/cache.

NOTE: If we change the order of the steps and re-run the build command, Docker won't use cache for any steps starting at the line where the change took place.

Tagging an Image


docker build command gives us an ImageID but its harder to remember. So, we can add a name to the image during build and refer to it during our run command.

docker build -t <my-image-name>:<version> .

NOTE: adding a version number is a good practice to differentiate updates made to Docker configuration.

docker run <my-image-name>

NOTE: if no version is specified, the latest version will be used automatically by Docker.

Simple NodeJS Example

Let's try to run a simple nodejs server on our local machine using Docker and Dockerfile. After creating a simple nodeJS project and installing express in it, create a simple GET route which sends back "Hello World". Then create a dockerfile in the root of the project.

#dockerfile in the root of the project

FROM node:alpine

RUN npm install

CMD ["npm","start"]

FROM --> we specified an "alpine" image for NodeJS which contains bare bones installation of Node and some other default commands.

RUN --> we will need all dependencies installed

CMD --> finally we will start up our server

The above Dockerfile will throw an error though because it won't find package.json file. Why??

Remember Docker creates a container which is isolated and has no access to our local machine. Even though the Dockerfile sits in the root of our project, it doesn't have access to any files in that project.

We need to make our files available to Docker for it to run the commands successfully. This can be achieved by copying over the files to Docker Container like so:

COPY . .

The COPY command takes in 2 arguments - the source location and the destination. In our case, we want to copy everything from the root of our project to the root of the Docker Container.

#dockerfile in the root of the project

FROM node:alpine
COPY . .
RUN npm install
CMD ["npm","start"]

COPY


We will find that with the above dockerfile, if we make any change to our code base in NodeJS, Docker will run all steps again including npm install. This is not ideal on bigger projects where we may have many more dependencies and it will take long time to install all those. So, let's only copy package.json first and then all the rest of the files.

#dockerfile in the root of the project

FROM node:alpine

COPY package.json .
RUN npm install

COPY . .

CMD ["npm","start"]

The above step will ensure that dependencies are installed only if we make changes to package.json and not our own codebase.

WORKDIR


Currently, we are storing package.json file and node_modules folder at the root of our container which isn't ideal because it may create conflicts with other folders in that location.

Usually, its best to create your own folder for our app. We can achieve this with WORKDIR command which tells Docker the context of the current project inside the container.

FROM node:alpine

WORKDIR /home/myapp

COPY package.json .
RUN npm install

COPY . .

CMD ["npm","start"]

Now if we run docker build . docker will work in the folder we specified. If the folder doesn't exist, it will be created.

Port Mapping


Even though the above dockerfile will start a server, we won't be able to access it. The same pricinple as before - the container is isolated from our machine, so chrome or any other application on our machine won't find anything running on the port specified in our NodeJS project.

We need to tell docker on run time to re-route any traffic from the local port on our machine to its own container's port like so:

docker run -p <port-on-our-machine>:<port-inside-container> <imageName>

docker run -p 8000:8080 mySampleContainer

Docker get started

Docker run reference

Docker exec reference