KubeAcademy by VMware
Building Images with Dockerfile: Basic Syntax, App Launch, Tags, and Build Context
Next Lesson

In this lesson, we run through a few introductory examples to cover the basic syntax of Dockerfiles, configuring the application launch command, using tags appropriately, and controlling the build context.

Cora Iberkleid

Part Developer Advocate, part Advisory Solutions Engineer at VMware

Cora Iberkleid is part Developer Advocate, part Advisory Solutions Engineer at VMware, helping developers and enterprises navigate and adopt modern practices and technologies including Spring, Cloud Foundry, Kubernetes, VMware Tanzu, and modern CI/CD.

View Profile

Let's begin our lessons on Dockerfile. A Dockerfile is simply a script with the commands needed to assemble an image. Throughout the following lessons, we'll use a series of examples to build our knowledge about writing Dockerfiles and some best practices to keep in mind.

In this first lesson, we'll learn a handful of Dockerfile commands and how to execute your Dockerfiles to build images. We'll cover launching the application, using tags, and controlling the build context.

In this directory. We've got a simple application written in Go, and some sample Dockerfiles. If we run this application, we get usage instructions. If we run it again and provide a string, say world, it responds with, "hello world." Before we turn this into an image, let's examine our first Dockerfile. Notice the basic structure. It's simply a series of commands, which will be executed in order from top to bottom. Each command starts with a valid Dockerfile instruction. These aren't case sensitive, but by convention, they're capitalized for readability. We start with "from" instruction. This instruction sets the base image, which gives us a starting point to build on.

For this particular example, we need an environment where we can build and run our Go application. Lucky for us, this Golang image has a Linux OS and Go tools installed and it's publicly available on Dockerhub, So we can just refer to it by name.

We use a label to add some metadata that might be useful to us in the future in inspecting or filtering images. We use a work [inaudible 00:01:55] instruction, which creates a directory and CDs into it just to place our files in a directory of our choosing. Next we're simply going to copy our files to the image and run Go install. This will create an executable called Hello and place it in the path.

Now, there are two instructions that can be used to launch an application. Entry point and command. You can use either on its own or you can use them together. Entry point should launch a process and can include arguments. If entry point is not used, command can do the same. Otherwise, command simply provides arguments for the entry point. Any user provided command or arguments override the command, so the command functions as a default, whereas the entry point is always run.

So in this case, we're using entry point to launch the application and command to change the default behavior and return hello world instead of usage instructions. We can run this using the Docker Build command. This of course requires having the Docker CLI installed and the Docker Daemon as well. Docker Build requires one argument, which is the context for the Docker file. That copied.dot command in the Docker file is copying files from the context into the image. We need a context that has our source code and since the source code is in our current directory, we can just say dot. We also use the minus T flag, which stands for tag to provide a name for our image.

We can see right away that we're downloading the Golang base image. Since we didn't specify a registry or a repository name, Docker defaulted to pulling the image from the Docker hub library, namespace, which contains a set of curated repositories known as official images. There are other reputable sources of images. Just make sure you do your research and that you're getting your base image from a trusted source, or you can build your own. We can see the rest of the instructions have been carried out, including the downloading of a Go package that the application uses and the build is complete.

We can use the Docker images command to list the images on our local Docker Daemon. There's the Golang image that we downloaded and now that it's available locally, we won't have to wait for the download from Docker Hub anymore. We also see the new image that we built. You can see from the size of the new image that it includes everything in the base plus our app.

To run the image, we use the Docker run command and the name of the image. If we don't provide any arguments, we get "hello world,: which confirms that we were able to change the default behavior through our Docker file. And if we do provide an argument, let's say sunshine, we get, "hello sunshine." So, the entry point was still executed, but the command was overwritten. So, this image includes the full stack that the application needs, including Linux, the application executable, the extra Go package that it uses, environment variables are properly set up, even the launch command is configured. So, hopefully the old expression it works on my machine can become a thing of the past because we're able to essentially ship the environment with the application.

Let's talk about tagging. A tag is essentially an alias to the image ID. Notice that both of the images in our Daemon are tagged as latest, which is the default value. Latest implies that it's the most recent edition of a given image, but it's useful to assign tags to convey more specific information about the version or the variant. Let's assign some tags to our hello image. We'll tag it as version 1.6 and we'll also tag it as version family 1.

We can see that all three tags are pointing to the same image ID. So, no matter which tag you choose, at least at this moment in time, you'll get the same image. Let's make a change. Now, any change will produce a different ID, so we can simply change the default command in our Docker file for example. Let's rebuild the image and we'll tag it as version 1.7, version family 1, and the default of latest.

We can see the more generic tags have been reassigned to the new image ID and only 1.6 is still pointing to the previous image. So, users of our image can choose the tag that best fits their needs. For early development, a more generic tag might be desirable, but if they need to guarantee the exact image is built on a different machine or in a different time, they would need to use the most specific tag. Similarly, in our own Docker file, we can add a tag to ensure it will always get the same base image, no matter where or when we build our image.

It's also worth pointing out the difference between a tag and a label. Both provide information about an image, but the label is part of the image and is therefore immutable. The tag is a pointer to an image and it's mutable.

Let's talk about the build context. As we saw earlier, the Docker Build command requires us to specify a context and the context needs to contain all of the files that the Docker file needs in order to build the image. What I didn't mention is that the build isn't happening in our local directory and it isn't the CLI that's running our Docker file. Rather, the CLI is sending the Docker file and all of the files in the context to the Docker Daemon, and the Daemon is building the image. We can see evidence of this in the first line of the output of the Docker Build command.

So, if your context contains more than what you need, your build will be unnecessarily slower. In our case, the problems a bit more serious since we just copied everything from the context into the image. We can use this syntax to start a container and override the entry point. So, instead of launching our hello program, we'll just launch the shell. We're including the minus IT flag to make it interactive with a terminal and now we're inside the container, so we can see we're in the workspace directory by default. The default directory is determined by the last work [inaudible 00:08:45] instruction in the Docker file.

We also see a few files here that we don't need. Our Docker files and a read me file. These are adding unnecessary bulk and we've created a loophole for potentially confidential or malicious files to make it into our runtime environment. Now there's an easy fix for this, which is the dot Docker Ignore file. It's similar to a dot Get Ignore, and it enables us to filter what gets sent to the Daemon. In this example, we're excluding everything by default and allowing only what we want. Let's rebuild and we can see clearly that we're uploading less data than before. Now let's run the shell again and this time we'll add the commands in line here and just list the contents. You can see that now we have just the files we need.

That concludes our first lesson on Dockerfile. To recap, we learned about the basic structure of a Dockerfile using entry point and command to control the launch behavior, using tags wisely, and minimizing the build context to improve build speed and security.

Give Feedback

Help us improve by sharing your thoughts.

Share