#DockerDays Day 5 – Building Images with dockerfile

So far we have been downloading images from Docker Hub and running the containers. But how are these images created in the first place? How can we create our own images?

In this part of the series of Docker Days, we will delve into building images and how to use the Docker file.

Agenda

  • Introduction to dockerfile ?
  • Format
  • Build Image
    • A simple example
    • Build and Context
  • Multi-Stage Build

Introduction to dockerfile ?

Docker builds images by reading a set of instructions that is used to assemble the image. These instructions are stored in a text document, known as dockerfile. Let us look into an example.

FROM ubuntu:latest
RUN mkdir ./demo
COPY ./demofiles ./demo
CMD ["echo","image created"]

The above code or rather lines of instructions constitute a dockerfile which is used to build an image of ubuntu. Let us take a step backward to remember how images are constructed. Images consist of multiple stacked layers, each of the layer represented by an instruction in the dockerfile. Each layer in the stack is a delta of change from the previous layer.

Let us consider the above example set of instructions.

  • FROM : create a layer from ubuntu image with latest tag.
  • RUN : adds a directory named demo
  • COPY : copies the content of demofiles to newly created directory.
  • CMD : specifies the command to execute

Each of the instructions adds a layer on top of the previous one. The dockerfile aids in automating the build process of your image through simple and systematic steps that can be easily understood.

Format

A typical dockerfile contains two types of Instructions (followed by arguments) and Comments

The format can be given as

# Comment
INSTRUCTION arguments

Any line with begins with a # is considered comment in docker. The exception to this is the parser directivesInstructions are followed by arguments. While the instructions themselves are not case-sensitive, by convention they are often written in upper-case.

Build Docker Image

We now know-how dockerfile can be used to define the layers of docker image. But how does one build the actual image? This is where the docker build commands come into the party.

Simple Example

Let us go ahead and build docker image for the above defined docker file. Let us create a folder named demofiles and add a few files to it. This is mere to ensure these files are included in the image we build and are available when we run the container.

Our demo folder looks like

root
 |---demofiles
       |---demo.inf
 |---dockerfile

Let us now use the docker build command to build the image.

docker build -t docker.demo:latest .

The -t flag specifies the name and tag (name:tag format) associated with the image.

The image of ubuntu so created would now container a directory called demo, which in turn contain a file named demo.inf. We can verify this by running the container and executing bash commands.

$ docker run --name docker.demo.container -it docker.demo bash
$ ls
bin   demo  etc   lib    lib64   media  opt   root  sbin  sys  usr
boot  dev   home  lib32  libx32  mnt    proc  run   srv   tmp  var
$ cd demo
$ ls
demo.inf
$

The dockerfile is traditionally, located in the root of context. However, you can use the -f flag to specify a different location.

$ docker build -f /some/other/location/Dockerfile .

Build and Context

The build command executes the instructions the dockerfile sequentially based on the build context. Build Context is the set of files specified by a url or path. In the above example, we are using the current directory, specified by the ., as the context.

It is worth noting what happens when you execute the build command. When the command is executed by the daemon, the build process would send the entire context (recursively) to the daemon. Hence one needs to be careful of what is involved in the context. You can view this activity if you examine the log while building the image.

=> => transferring context: 68B

It is advisable to keep the context as minimalist as possible with only the files which is essential for the image. Including unnecessary files would result in a larger context and larger image size, which would also result in a larger container runtime size.

You can use the .dockerignore to skip or ignore the files from the context directory. The .dockerignore is a simple text file that would contain the file or directory name/paths which need to be excluded.

The build process also supports build caches to speed up the build process. By default, the result of previous builds (on the same machine) is used as build caches. However, build can use a distributed cache too. We will examine external cache sources in a different blog post. For now, if you re-run the build command, you can examine the log to understand how the build uses cache to speed up the results.

[+] Building 3.7s (8/8) FINISHED
 => [internal] load build definition from Dockerfile                                                               0.0s
 => => transferring dockerfile: 31B                                                                                0.0s
 => [internal] load .dockerignore                                                                                  0.0s
 => => transferring context: 2B                                                                                    0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                                                   3.6s
 => [1/3] FROM docker.io/library/ubuntu:latest@sha256:8ae9bafbb64f63a50caab98fd3a5e37b3eb837a3e0780b78e5218e63193  0.0s
 => [internal] load build context                                                                                  0.0s
 => => transferring context: 68B                                                                                   0.0s
 => CACHED [2/3] RUN mkdir ./demo                                                                                  0.0s
 => CACHED [3/3] COPY ./demofiles ./demo                                                                           0.0s
 => exporting to image                                                                                             0.0s
 => => exporting layers                                                                                            0.0s
 => => writing image sha256:1a9ff74226be2451a6de7ee298d2c6e1dc85ed06ad5e20a1ac32e6d3ff7896e6                       0.0s
 => => naming to docker.io/library/docker.demo:latest

Multi-Stage Build

In the previous example, we saw a simple example of building a container. We will take a step more and build a more realistic example now. We will create a VueJs app and build an image that would host the application on nginx.

Let us begin by creating a demo app.

vue create demo-app

We will not make any changes to the template created. After all, that’s not our intention. Let us now create a .dockerignore file and list out the files which we would like to exclude from the final image. Remember, ideally we would like to keep the size of final image to be minimal and hence it should not container any files which are unnecessary.

# .dockerignore - add files to be excluded here
**/node_modules
**/dist

If you notice we have included /dist folder, but that is because we would be using an explicit COPY command to copy the necessary files around.

We will now commence towards defining our dockerfile.

#build
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

#production
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d
EXPOSE 80
CMD [ "nginx","-g","daemon off;"]

The dockerfile which we have created in the above example consists of two parts – or better called two stages. Multistage dockerfile enables developers to organize and optimize the dockerfile better. In the above example, we have isolated the build and publish process.

Multi-stage dockerfiles benefit from their ability to have derive from multiple base files and use output of different layers in subsequent(or final) layers. This can aid in cutting down the final size of image. In the above example, you have only copied the build binaries (we do not need everything from the node image) from the first stage to the second, thereby reducing the size.

We will discuss the benefits of the multi-stage dockerfile in later parts of this series. The objective of this introductory post on dockerfile intends to familiarize you with different concepts of building docker images. We will delve into details soon in our upcoming posts.

Conclusion

In this post, we familiarized ourselves with the key elements of building an image. We also understood the basic skeleton of an image file. But this is only beginning, in the next post, we will delve more into the docker file and understand how we could squeeze more from the docker file.

2 thoughts on “#DockerDays Day 5 – Building Images with dockerfile

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s