How To Use A Dockerfile

How To Use A Dockerfile

In the earlier Docker articles, we were able to build images and get them running as containers manually. In this tutorial, we will explore how to take this a step further by building images with code. This is where the Dockerfile comes into play. Dockerfiles are small programs that describe how to assemble a Docker image. You can run these small programs using the docker build command. Once the docker build command finishes running the Dockerfile, a new Docker image will be placed into the local Docker Registry on your computer. Once you have a suitable image, you can run it with the docker container run command.


What Is A Dockerfile?

A Dockerfile is a recipe for creating your image with a default name of Dockerfile with a capital D. If you want to name your Dockerfil something else, you can do that with the -f option. Each statement in the Dockerfile is its own layer and the order of them matters since the file runs top-down.

  • A Dockerfile is like a small “program” to create an image.
  • This small program is run with:
    • docker build -t name-of-result .

Each Step Produces An Image

  • Each line takes the image from the previous line and makes another image.
  • The prior image is unchanged.
  • The state on the previous line is never edited.

Each step of the build process produces a new image. The build sequence is a series of steps where it starts with one image, makes a container out of it, run something in it, make a new image. The image in the prior step is unchanged. Each next step is saying, start from the prior step, make some new changes, then save to a new image. Dockerfiles are not shell scripts though they do look like shell scripts, because that helps them be familiar to people, and it makes them a little easier to learn. Processes you start on one line will not be running on the next line. You run them, they run for the duration of that container, then that container gets shut down, saved into an image, and begin anew on the next line. We’ll run through several examples to see how this works.


A Basic Dockerfile In Action

To get started with a Dockerfile, we can begin with only three commands to keep things simple. You will find these commands in virtually all Dockerfiles you encounter.

  • FROM – Set the baseImage to use for subsequent instructions. FROM must be the first instruction in a Dockerfile.
  • RUN – Execute any commands on top of the current image as a new layer and commit the results.
  • CMD – Provide defaults for an executing container. If an executable is not specified, then ENTRYPOINT must be specified as well. There can only be one CMD instruction in a Dockerfile.

First, create a file named Dockerfile with no extension at all in the directory of your choosing.

dockerfile-tutorial> touch Dockerfile

Now, we can populate that file with the following commands.

FROM busybox

RUN echo "building a docker image"

CMD echo "hello from the container!"

Running A Dockerfile

Now comes the fun part. We want to run the Dockerfile we just created. Recall that this can be done with the docker build command which is a part of the builder management collection of the commands. The -t we include is simply tagging the image with a name so we have something to reference it by, otherwise, the resulting image would have only a cryptic id and no easily readable name. The other point to note is that there is a dot at the end of the command. That just says, use the Dockerfile in this directory we are in.

dockerfile-tutorial> docker builder build -t my-container .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM busybox
latest: Pulling from library/busybox
9758c28807f2: Pull complete
Digest: sha256:a9286defaba7b3a519d585ba0e37d0b2cbee74ebfe590960b0b1d6a5e97d1e1d
Status: Downloaded newer image for busybox:latest
 ---> f0b02e9d092d
Step 2/3 : RUN echo "building a docker image"
 ---> Running in 1ec98c10d7d4
building a docker image
Removing intermediate container 1ec98c10d7d4
 ---> 8b4d5802aed5
Step 3/3 : CMD echo "hello from the container!"
 ---> Running in 7c4e7b0aa45b
Removing intermediate container 7c4e7b0aa45b
 ---> 2cd663381747
Successfully built 2cd663381747
Successfully tagged my-container:latest

The image is built successfully and shows up in both Visual Studio Code and the Docker Dashboard.

dockerfile visualstudiocode

images in docker dashboard

Let’s go through what happened here. The first thing we see is “Sending build context to Docker daemon”. This is the Docker client sending the build context to the Docker daemon. That build context is the entire directory the Dockerfile is in. Following this, Docker will print out each step of the build process. In step one of three we have the following:

Step 1/3 : FROM busybox
latest: Pulling from library/busybox
9758c28807f2: Pull complete
Digest: sha256:a9286defaba7b3a519d585ba0e37d0b2cbee74ebfe590960b0b1d6a5e97d1e1d
Status: Downloaded newer image for busybox:latest
 ---> f0b02e9d092d

This step simply downloads and creates an image from busybox, which is very small and has nothing but a shell in it. Then the build process moves to step 2 of 3.

Step 2/3 : RUN echo "building a docker image"
 ---> Running in 1ec98c10d7d4
building a docker image
Removing intermediate container 1ec98c10d7d4
 ---> 8b4d5802aed5

During step two it starts from that image, creates a container, and runs the command provided. From that running container, the echo of “building simple docker image” is run. During this step the 8b4d5802aed5 image is created. There is an intermediate of 1ec98c10d7d4 which was used to run our build step. Docker is smart enough to know that this intermediate container is not going to be used anywhere else in the Dockerfile, so it cleans up the intermediate container. This brings us to step 3 of 3.

Step 3/3 : CMD echo "hello from the container!"
 ---> Running in 7c4e7b0aa45b
Removing intermediate container 7c4e7b0aa45b
 ---> 2cd663381747
Successfully built 2cd663381747
Successfully tagged my-container:latest

During the final step, Docker sets the CMD to be run when this container starts. It is updating the state of the image. This step resulted in another intermediate container 7c4e7b0aa45b which is used temporarily in order to commit to the final image. Like the prior step, once it is no longer needed, it is removed. So the final result is the 2cd663381747 image. This is it’s image id. Since we tagged (read – named) the image as part of the build process, we can see that my-container is referring to the 2cd663381747 image.


Running Your Image

The section above showed how to build the most basic of images in Docker by using a Dockerfile. The build went successfully, and we now see the image in our local registry. Well, images were made for running, so let’s go ahead and spin up a container from our newly built image!

dockerfile-tutorial> docker container run my-container
hello from the container!

WORKDIR and COPY Example

Now let’s build another image, but make it a little more interesting. We’re going to build and run an Nginx Container while also adding our own custom HTML and CSS into the container during build time. That way we can see our own HTML page rather than the default Nginx splash page. In the practice directory add these two files.

css.css

index.html

Dockerfile

FROM nginx:latest

WORKDIR /usr/share/nginx/html

COPY css.css index.html ./

The Dockerfile here uses nginx:latest in the FROM command. We’re not building our own Nginx, since an official image exists already that is battle-tested and works. Using official images in your Dockerfiles will make your Dockerfile easier to maintain as time passes. You can easily add functionality on top of an existing base image, and that is what Docker is for and how it works. Now we see the WORKDIR command in action. You can almost think of WORKDIR like running “cd /the/directory/path”. In the Dockerfile however, anytime you need to set the directory path you should use WORKDIR. In this example, we are setting the working directory to the default root HTML folder in Nginx. The last command we run is the COPY command which is used to copy your own source code files into container images. In our case, we are simply taking css.css and index.html, then placing them in the ./ directory (which refers to /usr/share/nginx/html) since we have set the path using WORKDIR already. Now, you can see that this Dockerfile is missing a required command of CMD at the end. This is because the FROM image (nginx) already has a CMD in it. Using FROM means you inherit everything included in that base image. You can see this by looking at the layers on Docker hub, or with the docker history command – both shown here.

Viewing image layers on Docker Hub.
view docker image layers

Viewing nginx image history.
docker history nginx

Before we build an image using this Dockerfile, let’s confirm we have no images on the system currently.
docker no images on disk

Now we can build the image and we’ll name it using -t just so we have a nice name to work with.

dockerfile-tutorial> docker build -t customnginx .
Sending build context to Docker daemon  4.096kB
Step 1/3 : FROM nginx:latest
latest: Pulling from library/nginx
bb79b6b2107f: Pull complete
111447d5894d: Pull complete
a95689b8e6cb: Pull complete
1a0022e444c2: Pull complete
32b7488a3833: Pull complete
Digest: sha256:ed7f815851b5299f616220a63edac69a4cc200e7f536a56e421988da82e44ed8
Status: Downloaded newer image for nginx:latest
 ---> f35646e83998
Step 2/3 : WORKDIR /usr/share/nginx/html
 ---> Running in a3c878027088
Removing intermediate container a3c878027088
 ---> 92aebe9e2888
Step 3/3 : COPY css.css index.html ./
 ---> 30abd7dc1d52
Successfully built 30abd7dc1d52
Successfully tagged customnginx:latest

The build went well, and we see the new image in Docker Desktop.
new image on disk docker desktop

At this point, we have built an image but do not have a running container. Let’s confirm.
docker getting started

Let’s run our new container!

dockerfile-tutorial> docker container run -dp 80:80 customnginx
d5e78a8c4d4947cc51773152c6040c13b82c6fd43a2f3dbf483b894b17e1f8b0

Ok, the container is running well!
customnginx container running

Now visit http://localhost and you can see the new custom nginx container!
the docker container works


Commonly Used Dockerfile Commands

In this section, let’s look at the most commonly used Dockerfile commands. The ones to pay the most attention to are FROM, ENV, RUN, EXPOSE, and CMD.


The FROM Statement

The FROM statement just says what image to start running from. This should always be the first expression in a Dockerfile. It’s actually okay to put multiples of them in a Dockerfile. It means that the Dockerfile produces more than one image.

Usage:

  • FROM <image>
  • FROM <image>:<tag>
  • FROM <image>@<digest>

Information:

  • FROM must be the first non-comment instruction in the Dockerfile.
  • FROM can appear multiple times within a single Dockerfile in order to create multiple images. Simply make a note of the last image ID output by the commit before each new FROM command.
  • The tag or digest values are optional. If you omit either of them, the builder assumes a latest by default. The builder returns an error if it cannot match the tag value.

Reference – Best Practices


The RUN Statement

The RUN statement says to run a command through the shell. For example RUN unzip the install.zip file into this directory or RUN hello docker.

Usage:

  • RUN <command> (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows)
  • RUN ["<executable>", "<param1>", "<param2>"] (exec form)

Information:

  • The exec form makes it possible to avoid shell string munging, and to RUN commands using a base image that does not contain the specified shell executable.
  • The default shell for the shell form can be changed using the SHELL command.
  • Normal shell processing does not occur when using the exec form. For example, RUN ["echo", "$HOME"] will not do variable substitution on $HOME.

Reference – Best Practices


The CMD Statement

This is required and is the final command that will be run every time you launch a new container from the image. It also holds true anytime you restart a stopped container.

Usage:

  • CMD ["<executable>","<param1>","<param2>"] (exec form, this is the preferred form)
  • CMD ["<param1>","<param2>"] (as default parameters to ENTRYPOINT)
  • CMD <command> <param1> <param2> (shell form)

Information:

  • The main purpose of a CMD is to provide defaults for an executing container. These defaults can include an executable, or they can omit the executable, in which case you must specify an ENTRYPOINT instruction as well.
  • There can only be one CMD instruction in a Dockerfile. If you list more than one CMD then only the last CMD will take effect.
  • If CMD is used to provide default arguments for the ENTRYPOINT instruction, both the CMD and ENTRYPOINT instructions should be specified with the JSON array format.
  • If the user specifies arguments to docker run then they will override the default specified in CMD.
  • Normal shell processing does not occur when using the exec form. For example, CMD ["echo", "$HOME"] will not do variable substitution on $HOME.

Reference – Best Practices


The LABEL Statement

Usage:

  • LABEL <key>=<value> [<key>=<value> …]

Information:

  • The LABEL instruction adds metadata to an image.
  • To include spaces within a LABEL value, use quotes and backslashes as you would in command-line parsing.
  • Labels are additive including LABELs in FROM images.
  • If Docker encounters a label/key that already exists, the new value overrides any previous labels with identical keys.
  • To view an image’s labels, use the docker inspect command. They will be under the "Labels" JSON attribute.

Reference – Best Practices


The ENV Statement

The Environment statement sets environment variables like ENV DB_HOST=db.production.example.com or DB_PORT is 5432.

Usage:

  • ENV <key> <value>
  • ENV <key>=<value> [<key>=<value> …]

Information:

  • The ENV instruction sets the environment variable <key> to the value <value>.
  • The value will be in the environment of all “descendant” Dockerfile commands and can be replaced inline as well.
  • The environment variables set using ENV will persist when a container is run from the resulting image.
  • The first form will set a single variable to a value with the entire string after the first space being treated as the <value> – including characters such as spaces and quotes.

Reference – Best Practices


The EXPOSE Statement

By default, no tcp or udp ports are open inside of a container. It doesn’t expose anything from the container to a virtual network unless it is listed here. The EXPOSE command does not mean that these ports are going to be automatically opened on your host, that is what the -p command is used for with docker container run.

Usage:

  • EXPOSE <port> [<port> …]

Information:

  • Informs Docker that the container listens on the specified network port(s) at runtime.
  • EXPOSE does not make the ports of the container accessible to the host.

Reference – Best Practices


The COPY Statement

Usage:

  • COPY <src> [<src> …] <dest>
  • COPY ["<src>", … "<dest>"] (this form is required for paths containing whitespace)

Information:

  • Copies new files or directories from <src> and adds them to the filesystem of the image at the path <dest>.
  • <src> may contain wildcards and matching will be done using Go’s filepath.Match rules.
  • <src> must be relative to the source directory that is being built (the context of the build).
  • <dest> is an absolute path, or a path relative to WORKDIR.
  • If <dest> doesn’t exist, it is created along with all missing directories in its path.

Reference – Best Practices


The ADD Statement

The ADD statement is a very useful expression that you can use to add a local file and perform other tasks. It can also add the content from an archive. So, if you say ADD project.tar.gz to /install, it doesn’t copy the file tar.gz into that directory, it notices that it’s a compressed archive and it uncompresses all the files in that archive to that directory. It also works with URLs. You can say ADD the thing that you download from this big URL to /project. That would cause project.rpm to get download and put in project/project.rmp.

Usage:

  • ADD <src> [<src> …] <dest>
  • ADD ["<src>", … "<dest>"] (this form is required for paths containing whitespace)

Information:

  • Copies new files, directories, or remote file URLs from <src> and adds them to the filesystem of the image at the path <dest>.
  • <src> may contain wildcards and matching will be done using Go’s filepath.Match rules.
  • If <src> is a file or directory, then they must be relative to the source directory that is being built (the context of the build).
  • <dest> is an absolute path, or a path relative to WORKDIR.
  • If <dest> doesn’t exist, it is created along with all missing directories in its path.

Reference – Best Practices


The WORKDIR Statement

Usage:

  • WORKDIR </path/to/workdir>

Information:

  • Sets the working directory for any RUN, CMD, ENTRYPOINT, COPY, and ADD instructions that follow it.
  • It can be used multiple times in the one Dockerfile. If a relative path is provided, it will be relative to the path of the previous WORKDIR instruction.

Reference – Best Practices


The VOLUME Statement

Usage:

  • VOLUME ["<path>", …]
  • VOLUME <path> [<path> …]

Creates a mount point with the specified name and marks it as holding externally mounted volumes from native host or other containers.

Reference – Best Practices


The ENTRYPOINT Statement

ENTRYPOINT is similar to CMD, but it specifies the beginning of the expression to use when starting your container and lets you tack more on the end. So, if your container has an entry point of LS, then anything you type when you say Docker run my image name would be treated as arguments to the LS command. CMD specifies the whole command to run, and if the person, when they’re running the container, types something after Docker run image name, that will be run instead of CMD. ENTRYPOINT gets added to when people add arguments to your container and CMD gets replaced when people add arguments to your container. You can actually use both of them together. If you have them both, they get strung together, one after the other. In general, if you’re trying to make something that looks like a program and you want people to not care that it’s running inside a Docker container, ENTRYPOINT is for making your containers look like normal programs. CMD is the more commonly used approach.

Usage:

  • ENTRYPOINT ["<executable>", "<param1>", "<param2>"] (exec form, preferred)
  • ENTRYPOINT <command> <param1> <param2> (shell form)

Information:

  • Allows you to configure a container that will run as an executable.
  • Command line arguments to docker run <image> will be appended after all elements in an exec form ENTRYPOINT and will override all elements specified using CMD.
  • The shell form prevents any CMD or run command line arguments from being used, but the ENTRYPOINT will start via the shell. This means the executable will not be PID 1 nor will it receive UNIX signals. Prepend exec to get around this drawback.
  • Only the last ENTRYPOINT instruction in the Dockerfile will have an effect.

Reference – Best Practices


The USER Statement

The USER statement says, I would like the commands run in this container to run as the user John, or maybe that user’s identified by number, USER 5000. This can be useful if you have shared network directories involved that assume a fixed username or a fixed user number.

Usage:

  • USER <username | UID>

The USER instruction sets the user name or UID to use when running the image and for any RUN, CMD and ENTRYPOINT instructions that follow it in the Dockerfile.

Reference – Best Practices


The ONBUILD Statement

Usage:

  • ONBUILD <Dockerfile INSTRUCTION>

Information:

  • Adds to the image a trigger instruction to be executed at a later time, when the image is used as the base for another build. The trigger will be executed in the context of the downstream build, as if it had been inserted immediately after the FROM instruction in the downstream Dockerfile.
  • Any build instruction can be registered as a trigger.
  • Triggers are inherited by the "child" build only. In other words, they are not inherited by "grand-children" builds.
  • The ONBUILD instruction may not trigger FROM, MAINTAINER, or ONBUILD instructions.

Reference – Best Practices


The ARG Statement

Usage:

  • ARG <name>[=<default value>]

Information:

  • Defines a variable that users can pass at build-time to the builder with the docker build command using the –build-arg <varname>=<value> flag.
  • Multiple variables may be defined by specifying ARG multiple times.
  • It is not recommended to use build-time variables for passing secrets like github keys, user credentials, etc. Build-time variable values are visible to any user of the image with the docker history command.
  • Environment variables defined using the ENV instruction always override an ARG instruction of the same name.
  • Docker has a set of predefined ARG variables that you can use without a corresponding ARG instruction in the Dockerfile.
    • HTTP_PROXY and http_proxy
    • HTTPS_PROXY and https_proxy
    • FTP_PROXY and ftp_proxy
    • NO_PROXY and no_proxy

Reference


The SHELL Statement

Usage:

  • SHELL ["<executable>", "<param1>", "<param2>"]

Information:

  • Allows the default shell used for the shell form of commands to be overridden.
  • Each SHELL instruction overrides all previous SHELL instructions, and affects all subsequent instructions.
  • Allows an alternate shell be used such as zsh, csh, tcsh, powershell, and others.

Reference


The HEALTHCHECK Statement

Usage:

  • HEALTHCHECK [<options>] CMD <command> (check container health by running a command inside the container)
  • HEALTHCHECK NONE (disable any healthcheck inherited from the base image)

Information:

  • Tells Docker how to test a container to check that it is still working
  • Whenever a health check passes, it becomes healthy. After a certain number of consecutive failures, it becomes unhealthy.
  • The <options> that can appear are…
    • –interval=<duration> (default: 30s)
    • –timeout=<duration> (default: 30s)
    • –retries=<number> (default: 3)
  • The health check will first run interval seconds after the container is started, and then again interval seconds after each previous check completes. If a single run of the check takes longer than timeout seconds then the check is considered to have failed. It takes retries consecutive failures of the health check for the container to be considered unhealthy.
  • There can only be one HEALTHCHECK instruction in a Dockerfile. If you list more than one then only the last HEALTHCHECK will take effect.
  • <command> can be either a shell command or an exec JSON array.
  • The command’s exit status indicates the health status of the container.
    • 0: success – the container is healthy and ready for use
    • 1: unhealthy – the container is not working correctly
    • 2: reserved – do not use this exit code
  • The first 4096 bytes of stdout and stderr from the <command> are stored and can be queried with docker inspect.
  • When the health status of a container changes, a health_status event is generated with the new status.

Reference


The STOPSIGNAL Statement

Usage:

  • STOPSIGNAL <signal>

The STOPSIGNAL instruction sets the system call signal that will be sent to the container to exit. This signal can be a valid unsigned number that matches a position in the kernel’s syscall table, for instance 9, or a signal name in the format SIGNAME, for instance SIGKILL.

Reference

All About Dockerfiles

We covered a lot of great topics in this Dockerfile tutorial. To get even more familiar with Dockerfiles and how to use them, check out the additional resources below.