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 withlatest
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 directives. Instructions
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”