14 minutes read

This topic's purpose is to introduce you to the fundamental idea of Docker, which is a Dockerfile. This is a set of instructions/commands that Docker uses to create images automatically. You will learn about its structure and study two very important instructions. In all sections, you will see hyperlinks to the official documentation. Open them and make sure to study them thoroughly. They complement the content provided here. So, let's start with an overview of Dockerfile.

Dockerfile overview

Dockerfile is the basis for creating images. With its help, you can describe what should be included in the image and what it should do using special instructions. Docker executes each instruction from top to bottom, line by line. Instructions should satisfy the following structure; INSTRUCTION argument(s). It can be preceded by an appropriate comment that will be removed by Docker before instruction execution. The instruction keywords are case-insensitive, but by convention they are written in uppercase letters. Combined with comments, each instruction resembles the following code snippet:

# Comment
INSTRUCTION argument(s)

Starting with this topic, we'll present a group of topics dedicated to instructions. You will learn about several fundamental yet important instructions in each of them.

FROM instruction

For each Dockerfile FROM is an instruction that you will see in the beginning. The only exception may be ARG instructions but you will learn about their purpose later on. The FROM instruction specifies the base image you want to use. Note that Docker image names must be lowercase.

Except for the ARG and comments the FROM instruction can be preceded by the escape parser directive.

Note that you have two options to specify the base image when working with FROM instruction:

  • Without a version tag: Consider you have a simple image like this:

    FROM ubuntu
    
    LABEL author=HyperUser
    
    ENTRYPOINT ["/bin/bash"]

    In such cases, Docker will find and pull the version of the image with the latest tag, that is ubuntu:latest. This can be beneficial as you get access to the latest features. The latest version might bring security or performance enhancements but there may be bugs or vulnerabilities too. But if you want a bug-free and secure experience, then you can choose a tried and tested stable version.

  • With a version tag: This option doesn't give you the latest updates but prevents different kinds of issues you might face with a newer image version. Your container will run with a fixed version, which avoids unexpected issues caused by changes in newer image versions.

FROM instruction also supports the AS <NAME> option. You can use this option to name a build stage when you have multistage builds. This feature will be covered in future topics.

LABEL instruction

In Dockerfile you can define labels to specify metadata inside the Docker image. The labels can be author info, environment and version info, and many more. These labels are key-value pairs with key=value format, where both key and value can be either enclosed in double quotes or without them:

FROM ubuntu:22.04
 
LABEL author=HyperUser
LABEL "application_environment"="development"
LABEL "version"=1.0
LABEL multi.first-key="first-value" multi.second-key="second-value"
LABEL first-key=first-value \
      second-key=second-value

 
ENTRYPOINT ["/bin/bash"]

If you inspect the image or container, the label information will be available in the Config section. Let's check the label information for the image created with the Dockerfile above. For that purpose, create ubuntu:v1 image from the Dockerfile above and execute the docker inspect ubuntu:v1 --format='{{json .Config}}' command. Combined with jq, it shows us the output in JSON format:

{
  "Hostname": "",
  "Domainname": "",
  "User": "",
  "AttachStdin": false,
  "AttachStdout": false,
  "AttachStderr": false,
  "Tty": false,
  "OpenStdin": false,
  "StdinOnce": false,
  "Env": [
    "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
  ],
  "Cmd": null,
  "Image": "sha256:616dd48aceb01a1e3874006bef404aac16abb4c6579d3da63d2448b8e6278271",
  "Volumes": null,
  "WorkingDir": "",
  "Entrypoint": [
    "/bin/bash"
  ],
  "OnBuild": null,
  "Labels": {
    "application_environment": "development",
    "author": "HyperUser",
    "key_1": "value_1",
    "key_2": "value_2",
    "multi.label1": "value1",
    "multi.label2": "value2",
    "version": "1.0"
  }
}

You can also define a label on a specific Docker object using the --label flag.

CMD instruction

The CMD instruction specifies the default command to run when a container is started from the image. It takes the command and its arguments for that purpose. In general, its preferred structure is CMD ["executable", "param1", "param2"…] but there are other approaches too. A Dockerfile can contain multiple CMD instructions, but only the last one takes effect.

Let's observe CMD in practice. Assume you have the following Dockerfile:

FROM ubuntu:22.04

LABEL author=HyperUser

# Shell form
CMD echo Hello Students.
# Exec form
CMD ["echo", "Hello World."]

As you can see there are two approaches to defining CMD instruction in a Dockerfile:

  • CMD echo Hello Students.: Runs the command in a shell. By default it is /bin/sh -c. The command enables shell features, such as piping, subcommands, signal processing, etc.

  • CMD ["echo", "Hello World."]: Runs the specified binary with provided args. This approach is more commonly used.

If you build the image and run a container on it, the output will only be Hello World, because the last CMD instruction overrides all previous ones. Besides specifying commands inside the Dockerfile, you also can pass the command when running the container. Let's do it with the image ID created on the Dockerfile above:

$ docker run d559b5d58841 echo Hi
Hi

This command will override the second CMD instruction and print Hi instead. It is crucial to understand this behavior since it is the main difference between CMD and ENTRYPOINT instruction.

ENTRYPOINT instruction

The ENTRYPOINT also targets the application in the image when you create the container. This instruction differs from CMD as passing arguments when running the container does not override ENTRYPOINT (although you can explicitly override it using the --entrypoint flag).

In practice, one of the use cases for ENTRYPOINT is combining it with the CMD instruction. You'll learn about this in the next section.

Before using this instruction, let's explore the shell form deeper. As you already know, when you use it, you execute the command using /bin/sh -c which expects a string. So echo Hello Students. is passed as a string to execute the /bin/sh -c 'echo Hello Students.' command. Assume you have the following Dockerfile:

FROM ubuntu:22.04

LABEL author=HyperUser

# shell form
ENTRYPOINT echo Hello Students.

Now, check what you'll see in the COMMAND column when listing containers:

$ docker ps -a --format "table {{.Names}}\\t{{.Command}}" --last 1
NAMES        COMMAND ...
hs-ubuntu-2   "/bin/sh -c 'echo He…"

This prevents you from passing one more argument to echo when you run the container. To avoid this, let's explicitly tell Docker to run the command with /bin/echo applying the exec form. By the way, you will see similar behavior if you use echo instead of /bin/echo for this example. After a small update, the Dockerfile will look like this:

FROM ubuntu:22.04

LABEL author=HyperUser

# exec form
ENTRYPOINT ["/bin/echo", "Hello Students."]

Now, let's use this Dockerfile to run a container by passing an argument to it. Notice that we use backticks (`) around date to execute the command on the host shell and pass its output to the container:

$ docker run --name hs-ubuntu-3 ubuntu:v3 `date`
Hello Students. Thu Nov 10 12:01:19 UTC 2022

As you can see, we didn't override the existing ENTRYPOINT but added one more argument to it. Now, let's check the last container info:

NAMES         COMMAND
hs-ubuntu-3   "/bin/echo 'Hello St…"

This time we applied the commands to /bin/echo as expected. Now let's try to combine the last two instructions.

Combining CMD and ENTRYPOINT

You can combine both CMD and ENTRYPOINT if you want to define the main command of the image and have the ability to define flags for it and override them when you need them. Let's prepare an image to see this structure in practice:

FROM ubuntu:22.04

LABEL author=HyperUser

ENTRYPOINT ["echo"]
CMD ["Hello","Students."]

If you run a container based on the Dockerfile you'll have a base echo command that can't be overridden. On the other hand, you can either run the container with parameters defined inside the Dockerfile or pass new parameters when running the container.

Conclusion

With this topic, you have stepped into the world of Dockerfile instructions. You explored some crucial instructions that will help you while using Docker. Let's sum up the topic:

  • FROM instruction lets you specify the base image.

  • You can use LABEL instruction to specify metadata inside an image.

  • The CMD instruction allows you to run applications inside images.

  • ENTRYPOINT instruction differs from CMD because it is not overridden by default when you pass arguments at container startup (although it can be overridden explicitly).

41 learners liked this piece of theory. 14 didn't like it. What about you?
Report a typo