Dockerfile Tutorial - Building Docker Images for Containers

Sarath Pillai's picture
Dockerfile instructions for image

As I mentioned in my earlier tutorials, Docker images are the source code for our containers. Images are the main building block of a container. In this tutorial we will learn about building our own Docker Images.

Before going ahead with this tutorial, if you are new to Docker, I recommend reading all of the below posts.

 

Read: Hypervisor vs Containers

Read: How to Install Docker

Read: Running Docker Containers

Till now, in all of our tutorials, we have used a readymade image available from Dockerhub. But the real use case of Docker will be to build an image with our required applications and configurations inside. We can use the existing images from Dockerhub as a base for our own images, but will have to add our own required configurations and packages and other required components on top of it.

 

Dockerfile is nothing but the source code for building Docker images. 

 

You can also build a docker image using the simple docker commit approach, in which you simply launch a container from any existing image, make the required changes in the running container by executing commands inside the container, and then committing it with a new tag.

However that approach of committing containers and building images is not a recommended way of building docker images. This is because, Dockerfile gives you the advantage of building your container anytime, without doing it all yourself. Also Dockerfile gives an automated method of making containers in your build and deployment pipeline.

 

Creating our First Dockerfile

Let's create a directory called "docker-file", and create our Dockerfile inside that directory (this is simply for easy building of our image.)

 

spillai@docker-workstation:~$ mkdir docker-file
spillai@docker-workstation:~$ cd docker-file/

Now let's create a Dockerfile

spillai@docker-workstation:~/docker-file$ touch Dockerfile

 

Now let's write some instructions inside our Dockerfile, so that we can make our image. An example instruction set is shown below..

 

spillai@docker-workstation:~/docker-file$ cat Dockerfile
FROM ubuntu:12.04
MAINTAINER Sarath "sarath@slashroot.in"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Our first Docker image for Nginx' > /usr/share/nginx/html/index.html
EXPOSE 80
spillai@docker-workstation:~/docker-file$

 

If you look closely at the contents of our Dockerfile, its nothing but a collection of instruction's with different arguments to them.

Instructions should be in Uppercase format (for example see RUN, EXPOSE, FROM, MAINTAINER etc). Each of the instructions in the Dockerfile, are processed in the exact same order you defined. Each and every instruction set in the Dockerfile adds an additional layer to the image and then does a commit.

Now, let's create our image from the above Dockerfile.

 

spillai@docker-workstation:~/docker-file$ sudo docker build -t="spillai/test_nginx_image" .
Sending build context to Docker daemon  2.56 kB
Sending build context to Docker daemon
Step 0 : FROM ubuntu:14.04
 ---> 9cbaf023786c
Step 1 : MAINTAINER Sarath "sarath@slashroot.in"
 ---> Using cache
 ---> 912452fdbe6d
Step 2 : RUN apt-get update
 ---> Using cache
 ---> d711127e4d76
Step 3 : RUN apt-get install -y nginx
 ---> Running in 4fab72b24686
Processing triggers for libc-bin (2.19-0ubuntu6.3) ...
Processing triggers for sgml-base (1.26+nmu4ubuntu1) ...
 ---> b9f58e96b137
Removing intermediate container 4fab72b24686
Step 4 : RUN echo 'Our first Docker image for Nginx' > /usr/share/nginx/html/index.html
 ---> Running in 1d1702c4dae4
 ---> c46b140fd8ad
Removing intermediate container 1d1702c4dae4
Step 5 : EXPOSE 80

 ---> Running in a98f7685870a
 ---> 728d805bd6d0
Removing intermediate container a98f7685870a
Successfully built 728d805bd6d0

 

First, let's see the build command that we used to create our new image. the -t="spillai/test_nginx_image" tag's the image. Basically its saying this image is part of the repository spillai and image name is test_nginx_image (do not worry much about this at this time..as we can change the tag anytime at a later point, after building the image.)

 

Please note the "." at the end of the #docker build command, it says, that the build operation should be executed in the current directory.

 

The workflow of Dockerfile and building is very simple. It goes like this....

  1. Docker first run's the container that you mentioned us FROM (in our case its ubuntu:12.04). So docker first pulls down that image ,and run's it.
  2. It then looks for the first instrction after that, and after executing that, it does something similar to #docker commit, so that it saves a layer with what is done till that instruction.
  3. Docker run's a new container with the last commited image (ie. The image created from the previous instruction step in the Dockerfile. The Running in 1d1702c4dae4  type of messages during the build command showed in the above example indicates this.)
  4. It then executes the next instruction in the Dockerfile, and again does a commit and creates another image. So that the very next instruction is executed inside this new image commited..And this process continues till the very last instruction in the Dockerfile.

 

So basically if one of your instruction in the Dockerfile fails to complete successfully, you will still have an image (that was created during the instruction before) that is usable.

This is really helpful for troubleshooting to figure out, why the instruction failed. ie: You can simply launch a container from the last image created during the build operation and debugg the failed instruction in Dockerfile by manually executing it.

 

spillai@docker-workstation:~/docker-file$ sudo docker images | grep nginx
spillai/test_nginx_image                                                 latest                                                 728d805bd6d0        14 minutes ago      232.1 MB
spillai@docker-workstation:~/docker-file$ sudo docker history 728d805bd6d0
IMAGE               CREATED             CREATED BY                                      SIZE
728d805bd6d0        14 minutes ago      /bin/sh -c #(nop) EXPOSE map[80/tcp:{}]         0 B
c46b140fd8ad        14 minutes ago      /bin/sh -c echo 'Our first Docker image for N   33 B
b9f58e96b137        14 minutes ago      /bin/sh -c apt-get install -y nginx             18.1 MB
d711127e4d76        18 minutes ago      /bin/sh -c apt-get update                       21.19 MB
912452fdbe6d        18 minutes ago      /bin/sh -c #(nop) MAINTAINER Sarath "sarath@s   0 B

 

The above shown #docker history command is helpful in reviewing how a container was built. It shows each image id, that were created during each instruction in Dockerfile, and the command that was executed to create all intermediate images to create the final image from. The argument that docker history command requires is only the image id(in our case, its the test_nginx_image that we just created).

 

Now let's run our newly created nginx test image as a container. This can be done as shown below.

 

spillai@docker-workstation:~/docker-file$ sudo docker run -d -p 80:80 --name my_nginx_test_container spillai/test_nginx_image nginx -g "daemon off;"
356389b43b02c5afb55de8145cb33a3e6539c671a97c2c6974a6308f1d7bac8d

 

 

In the above command we have launched a container out of our newly created image with a new container name of my_nginx_test_container. The argument spillai/test_nginx_image is the image name(you can also use the image id instead), and the final this nginx -g "daemon off;"  is the container command(it will start nginx and the container will keep running till this nginx process is running). The -p 80:80 option will bind the host port 80 to the container port 80. So you will be able to see our default web page of "Our first Docker image for Nginxby simply visiting the IP address of our docker host.

 

The last argument we gave to the docker run command is the command that needs to be executed inside the container, in our case its the nginx process. But giving that command to docker run is not the correct way of doing it. We should include that as well inside our Dockerfile, so that our nginx process gets started while running a container using our image.

This is achieved using CMD argument in Dockerfile as shown below. Let's add a new argument in our Dockerfile called CMD.

 

spillai@docker-workstation:~/docker-file$ cat Dockerfile
FROM ubuntu:14.04
MAINTAINER Sarath "sarath@slashroot.in"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Our first Docker image for Nginx' > /usr/share/nginx/html/index.html
CMD ["/usr/sbin/nginx", "-g", "daemon off;"]
EXPOSE 80

 

 

The above CMD argument inside our Dockerfile has the exact same parameter that we used during #docker run command. This will start the nginx process as soon as the container is started. Now build your image again.

Please note the fact that you need either remove the existing image using #docker rmi <image id>, or build this new one with a new tag as shown below.

spillai@docker-workstation:~/docker-file$ sudo docker build -t="spillai/test_nginx_image:1.0" .

 

We have used a new tag for our modified image, to avoid conflict with the existing image. Our image is now called spillai/test_nginx_image:1.0 (1.0 is the version tag i used as an example). Now let's run a container using this new image.

 

spillai@docker-workstation:~/docker-file$ sudo docker run -d -p 80:80 --name my_nginx_test_container spillai/test_nginx_image:1.0

 

As shown above, we have not used the command to run nginx this time during docker run(but still nginx will get started automatically, as we have provided that part during our image building process with CMD argument in Dockerfile). Please don't forget to stop and remove our old container my_nginx_test_container(with #docker stop my_nginx_test_container and #docker rm my_nginx_test_container) before running the above run command, or else docker will complain that a container with that name already exist.

 

What will happen if we still give a command during #docker run?

 

If, however, we still give a command during #docker run, docker will ignore the CMD in the image, and will execute the command you gave during the run.

 

What happens if i give multiple CMD argument's inside my Dockerfile?

 

Well docker only accepts one CMD inside the Dockerfile. If you have more than one CMD inside Dockerfile, Docker will use the last CMD in the Dockerfile.

 

So what if i want to run multiple processes inside my container using Dockerfile CMD?

In that case, you cannot use CMD for all your required commands. You should then use some management tools like Supervisor for managing multiple processes inside the docker container. And once supervisor is configured, use CMD for your supervisor process (so that supervisor intern will take care of all your required processes inside the container)

 

We saw that #docker run command can override whatever command was given to CMD in Dockerfile. If we need something that cannot be overridden using #docker run, then we will have to use a new argument inside our Dockerfile called "ENTRYPOINT", instead of CMD.

 

There is another advantage of using ENTRYPOINT instead of CMD. With ENTRYPOINT, the command that you pass during #docker run is passed as additional parameter's to the command you specified in ENTRYPOINT. So our Dockerfile with ENTRYPOINT, instead of CMD will now look like this..

spillai@docker-workstation:~/docker-file$ cat Dockerfile
FROM ubuntu:14.04
MAINTAINER Sarath "sarath@slashroot.in"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Our first Docker image for Nginx' > /usr/share/nginx/html/index.html
ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"]
EXPOSE 80

 

 

Now create a new image using #docker build command as we did previously with a new tag to avoid conflict(like say for example spillai/test_nginx_image:2.0), and then stop and remove the existing container my_nginx_test_container and launch a new one with #docker run (using our newly created image with ENTRYPOINT)

 

You can actually forcefully override even ENTRYPOINT defined in Dockerfile during run command by using --entrypoint flag.

 

Some useful Dockerfile arguments

 

ENV

Its used to set environment variables right inside your Dockerfile for your containers launched used that image. The format of ENV is simple, and accepts a key/value pair.

ENV MY_PATH /opt

Or you can alternatively define it as below.

ENV MY_PATH=/opt

The subsequent run instructions, that you place beneath the ENV argument can now use that variable. You can also pass your required environment variables during the docker run command with -e flag (multiple environment variable's using multiple -e flags.

 

ADD

This ADD instruction inside Dockerfile is used to add files and directories from our docker build environment inside our image. Let's say we need to copy our index.html in our example nginx container from our local directory (the place from where we ran docker build command), we can do that as shown below.

So now we canactually replace our nginx Dockerfile with the below.

FROM ubuntu:14.04
MAINTAINER Sarath "sarath@slashroot.in"
RUN apt-get update
RUN apt-get install -y nginx
ADD index.html /usr/share/nginx/html/index.html
ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"]
EXPOSE 80

If you see the above Dockerfile, we have now used ADD instead of the old echo redirection statement for our index.html page. This would copy the index.html in the current directory (from where you ran the docker build command with "." to /usr/share/nginx/html/index.html).

ADD has a very simple format. ADD <Source> <Destination>

The source can be a URL, File or even a directory. The main requirement is, it should reside inside the correct build location. This is because the build context is passed to docker, during the first step while building an image using Dockerfile.

ADD http://apache.petsads.us/tomcat/tomcat-8/v8.0.21/bin/apache-tomcat-8.0.21.tar.gz /opt/apache-tomcat-8.0.21.tar.gz

As shown above, ADD can also be used for downloading a file from a URL and placing it inside the image.

 

One cool feature of ADD is the automated unpacking of compressed files.

ADD apache-tomcat-8.0.21.tar.gz /opt/tomcat/

The above will untar the apache-tomcat-8.0.21.tar.gz(from the local build directory) to the directory /opt/tomcat inside the container.

This feature wont work if the source is URL. Another thing to note about this is that, if there is a file/directory in the destination that has the same name, it will not be overwritten. Also if the target destination does not exist, Docker will create it. And the files extracted will be of permission set 0755.

 

COPY

 

COPY is very much similar to ADD. The only main difference is that COPY is primarily used for copying files (from where docker build happens) to images. Also it does not have the decompression features available with ADD.

COPY conf/ /usr/share/nginx/html/

 

The above will simply copy the files inside conf directory (ofcourse from the build context) to /usr/share/nginx/html directory.

If the source happens to be a directory. The entire directory will be copied to the image with exact same metadata. Also note the fact that files and directories created using COPY will be with GID and UID of 0.

 

VOLUME
 

This instruction is mainly used to add volume's to the container launched using the image(or simply put it..it adds a mount point). This is a really good method for persistent data. As you know that container does not have any persistence in terms of data. And containers cannot access data inside another container.  Using this instruction we can create a volume for a container that has the following properties.

  • Multiple containers can access the volume
  • A volume of a stopped container can also be accessed by another container.
  • Volumes created like this will persist, till any one container uses/references it(even stopped containers).
  • Also changes to the volume, will not be part of the image, while updating the image.

This is a really nice feature, because we can place things like db, source code, inside the volume, without making that part of the image. And the main added advantage is that other container's can use this volume as well..

The syntax of VOLUME in Dockerfile is simple, and is shown below.

VOLUME ["/opt/data"]

 

The above will add a volume /opt/data to the container. You can add multiple volumes as shown below.

VOLUME ["/opt/mydata", "/data" ]

 

 

This VOLUME instruction does not tell what to mount from local system. This only tells docker that the whatever placed in that volume location will not be part of the image. And will be accessible from any other containers as well.

 

#sudo docker run -d --volumes-from data_container -p 80:80 --name my_nginx_test_container spillai/test_nginx_image

The above command will make all the volumes of container "data_container" accessible to our my_nginx_test_container.

 

ONBUILD

We have always used a FROM statement while writing Dockerfile. There are cases where you need to create a base image for your application containers. Say for example a python application for each of your environment. Say Dev, Qa and UAT. In that case its always good to have a base image prepared and create environment specific images from your base image.

Say for example, you have an image created with the name spillai/my_base_python and all other python application images will reference this base image in FROM instruction.

So it will start with FROM spillai/my_base_python. But you might want to make sure that some predefined env specific steps are executed, when any new image is created using FROM spillai/my_base_python.

This is where ONBUILD instruction of Dockerfile comes into picture.

Keep in mind that the ONBUILD instructions are something that will be executed at a later time. In other words, things that you need any future image builds ( that uses this image as base with FROM ) to execute.

 

The syntax of ONBUILD looks like the below.

ONBUILD ADD . /var/www/

 

ONBUILD accepts any other Dockerfile instructions as parameter's. However, you cannot use MAINTAINER, FROM, and ONBUILD itself as parameter's to ONBUILD.

ONBUILD instruction set is executed as soon as FROM instruction is executed. This is a good thing, because any other instructions might require the ONBUILD instructions to be executed first. Say for example, you might need to copy new source code to /opt/myapp directory before a compilation happens in your new image. In that case our ONBUILD instruction inside our base image will trigger when a new image is built using that as base, before any other instruction in the Dockerfile is executed for creating the new image.

 

WORKDIR

Its the simplest instruction inside a Dockefile. It sets the working directory for any instruction that you want inside the Dockerfile. It can ofcourse be used multiple times in a single Dockerfile (because RUN, COPY, ADD and CMD commands might require different working directory).

WORKDIR /opt/myapp/
RUN command arguments
WORKDIR /opt/myapp/bin
ENTRYPOINT [ "examplecommand" ]

 

The above shown example will execute the RUN command inside /opt/myapp/ directory, and the ENTRYPOINT will be executed inside /opt/myapp/bin directory.

Rate this article: 
Average: 3.5 (133 votes)

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.