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.
- Introduction to dockerfile ?
- 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
- RUN : adds a directory named demo
- COPY : copies the content of
demofilesto 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.
dockerfile contains two types of
Instructions (followed by arguments) and
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.
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 .
-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 $
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
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
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
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
#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.
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.