---
title: Build a basic Docker image
date: 2022-02-04T06:01:00-05:00
author: John Morton
canonical_url: "https://supergeekery.com/blog/making-a-basic-docker-image"
section: Blog
---
# Build a basic Docker image

*February 4, 2022* by John Morton

![Make a boat](https://static.supergeekery.com/site-assets/make-a-boat.jpg)
*Illustration: The Squeezebox, designed by Jeff Gilbert, is novel lifeboat designed to be a safer and better alternative to the inflatable life raft.*

In this post, we’ll dissect a small Docker image I have on Docker Hub: [johnfmorton/tree-cli](https://hub.docker.com/r/johnfmorton/tree-cli).

It's not a complex image, but its simplicity is good because it is fairly easy to understand. Once you understand the basics, you can create your own more complex Docker images.

The _tree-cli_ image is based on an official Node image and has one customization: I've installed [tree-cli](https://www.npmjs.com/package/tree-cli) from npm, which is a package that allows us to list files in a tree structure. For example, directory listings look like this when using tree-cli.

```bash
/app
├── sample-file-1.md
├── sample-file-2.txt
└── subdirectory
   ├── file-1.txt
   └── file-2.jpg

directory: 1 file: 4
``` 

It's so simple, you may wonder _why a Docker image would need to be created at all_! 

If you wanted to install `tree-cli` on your own machine, you could use npm and install it locally from the command line and it would probably work just fine.

```bash
npm install -g tree-cli
```

I'm not doing that though because I'm trying to minimize apps I install directly on my computer. For the full story on this method of _not installing_ things, read [Dock Life](https://nystudio107.com/blog/dock-life-using-docker-for-all-the-things), a post by Andrew Welch. It's good stuff. You'll see the `tree` command mentioned in there. 

## The Docker Hub page vs the Github repo page

If you look at the Docker Hub listing for [johnfmorton/tree-cli](https://hub.docker.com/r/johnfmorton/tree-cli), you will find the actual image, but the thing you're missing is the Dockerfile which is the document that I used to create the image. The closest you get to that is looking at the layers of the image.
![Docker hub image layers](https://static.supergeekery.com/site-assets/docker-hub-image-layers.png)
*A screenshot of the layers from Docker Hub for tree-cli*
The layers have a lot of helpful information but looking at the Dockerfile is much easier to understand. I've posted the whole project, including the Dockerfile, at Github, [https://github.com/johnfmorton/docker-tree-cli](https://github.com/johnfmorton/docker-tree-cli). 

Let's take a look at the Dockerfile as a whole and then review it line-by-line. 

```docker
ARG TAG=16-alpine
FROM node:$TAG

WORKDIR /app

# Install dependencies
RUN set -eux; \
    npm install -g tree-cli

# I could have used the /bin/sh shell to give user a prompt by default
# CMD ["/bin/sh"]
# But I prefer to use "tree" to see the directory structure
CMD ["/usr/local/bin/tree"]
```
## Reviewing the Dockerfile

I told you it was short. 

Let's start at the very beginning [:musical_score:](https://www.youtube.com/watch?v=drnBMAEA3AM) and review the instructions on each line of the Dockerfile in order to be explicit about what's happening here.
### ARG line

```docker
ARG TAG=16-alpine
```

`ARG` is an argument, i.e. a variable we're creating with the name `TAG`. You might have guessed that. The [Docker documentation](https://docs.docker.com/engine/reference/builder/#arg) says "the `ARG` instruction 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."

In other words, we're going to create an argument, `TAG`, that may be useful later when we build this image. If you don't use it, the variable will contain the string "16-alpine" as the default.
### FROM line

```docker
FROM node:$TAG
```

`FROM`, according to the [docs](https://docs.docker.com/engine/reference/builder/#from), "sets the Base Image for subsequent instructions." The _base image_ is our starting point. There are many base images, official and unofficial, available on Docker Hub. Official images are, according to [the docs](https://docs.docker.com/docker-hub/official_images/), "a curated set of Docker repositories hosted on Docker Hub". Check out the [official base image page at Docker Hub](https://hub.docker.com/search?q=&type=image&image_filter=official) and you'll see many options: Ubuntu Linux, python, MySQL, Nginx, alpine, and more. You can confidently use these images because "Docker, Inc. sponsors a dedicated team that is responsible for reviewing and publishing all content in the Docker Official Images." ([link](https://docs.docker.com/docker-hub/official_images/#:~:text=Docker%2C%20Inc.%20sponsors%20a%20dedicated%20team%20that%20is%20responsible%20for%20reviewing%20and%20publishing%20all%20content%20in%20the%20Docker%20Official%20Images.))

There are even more _unofficial_ images as well. For example, the `tree-cli` image we're going over is an "unofficial" image. 

We need a node environment to run `tree-cli` because it's a node module, and we're going to use an [official Node base image](https://hub.docker.com/_/node)  called "16-alpine", which includes node version 16. I like that it's an official image. The term "[alpine](https://hub.docker.com/_/alpine)", when it comes to Docker, means that it's a small, minimal image, based on Linux, "[security, simplicity and resource efficiency](https://alpinelinux.org/about/#:~:text=security%2C%20simplicity%20and%20resource%20efficiency)".

What if you wanted to use "17-alpine" instead? We'll get to that when we build the actual image. (Spoiler alert: we'll use the `TAG` mentioned earlier.)
### WORKDIR line

The WORKDIR, [as the docs say](https://docs.docker.com/engine/reference/builder/#:~:text=sets%20the%20working%20directory%20for%20any%20RUN%2C%20CMD%2C%20ENTRYPOINT%2C%20COPY%20and%20ADD%20instructions%20that%20follow%20it%20in%20the%20Dockerfile), "sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile."

The line in our Dockerfile, `WORKDIR /app`, means we are setting the directory `/app` as the base directory from which the rest of the commands we issue will be run. To be clear, this is _insde_ the container that a directory called `/app` will exist.
### RUN line

Let's refer to the [documentation](https://docs.docker.com/engine/reference/builder/#run) again. "The RUN instruction will execute any commands in a new layer on top of the current image and commit the results." That means we're going to run a command as we're building this Docker image. This will customize the base image by doing _something_ to it. In our case, we want to add `tree-cli` to that base image. Before we do that though, we've got this `set -eux;` thing to deal with. What's that?

We're not going to reference the Docker documentation for the `set` command. We're going to reference [documentation](https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin) for `bash` for `set`. 

_Confused as to why we're not referencing Docker documentation?_ At this point in the process, imagine you've got your terminal open in this brand new, clean Linux machine. You're at the **bash** command line so that's our current frame of reference. Let's see what the bash documentation says about `set`.

> **set** allows you to change the values of shell options and set the positional parameters, or to display the names and values of shell variables.

So we're going to set some _options_ for the bash command (i.e. an npm install command). We're setting 3 options, `e`, `u`, and `r`. We're combining them all together with `-eur;`. Let's see what each option does.

* `-e` : exit immediately if a command returns a non-zero status
* `-u` : treat unset variables and parameters other than the special parameters ‘@’ or ‘*’ as an error 
* `-x` : print a trace of simple commands

In short, we're trying to prevent errors and print out as much helpful info to the terminal as we can when we execute our command.

Finally, we've ended this line with the `;` and then `\`. The semicolon is just basically ending the set command. The backslash is a sort of "new line" command that let's us execute additional commands without creating a new layer in the Docker image. . [There are more best practices like this in the Docker docs](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#minimize-the-number-of-layers), but basically, we're doing this to keep our Docker image as small as possible.
### The 'npm install -g tree-cli' line

We're finally installing something on our Docker image. Since we're being verbose here though, let's talk about how we knew to use this exact command. We're going to look at the npm page for `tree-cli`, specifically where it mentions [installing](https://www.npmjs.com/package/tree-cli#install) the software. 

```bash
npm install -g tree-cli
```
### Comment lines in the Dockerfile + CMD line

I'm going to treat the rest of the file as a single line. The lines that begin with a `#` are comments. They are here for reference only, so let's talk about the `CMD` command since it's the next active line in the Dockerfile. Let's go back again to the Dockerfile [documentation](https://docs.docker.com/engine/reference/builder/#cmd) and see what it says.

If you visit that link, there is quite a bit of information there. Here are the important bits for our image.

> The main purpose of a CMD is to provide defaults for an executing container.

I hear you! _What do those words mean?_

Let's imagine ourselves just a few minutes into the future when you've got a completed Docker image that does just one thing: it can list out the directory using the `tree` command because it has `tree-cli` installed on it. That's basically **all it can do**.

So, what do we want the default command to be when it's run? I think we should run the "tree" command. The `CMD` lets us set the default command to run if nothing else is specified when a user tries to use our Docker image.

```docker
CMD ["/usr/local/bin/tree"]
```

If you're wondering why it says `/usr/local/bin/` before `tree`, think about the npm install process. The `npm install` command installs programs into the `/usr/local/bin` directory, so we're just pointing to where `tree` lives on our little itty bitty Linux box.

Optionally, as you can see from my comments in the Dockerfile, we could have defaulted to just showing a bash prompt. I chose not to do that but left the comment tag in because it was a useful reference for me when I look at the file.

```docker
# I could have used the /bin/sh shell to give the user a prompt by default
# CMD ["/bin/sh"]
```
## Building the image locally

In case you're playing along on your computer, let's make sure we've got everything aligned. 

A directory on your computer contains the `Dockerfile` we've been working on above. The filename is just `Dockerfile`. It's not `Dockerfile.txt` or `dockerfile` or anything else. From within the terminal, you have navigated into that directory. You can confirm this by typing the `ls` command and you'll see the `Dockerfile` in there. Good?

We will next use use the `build` command. Here is the URL for the [documentation on the build command](https://docs.docker.com/engine/reference/commandline/build/). There is a lot of information there. We're only going to touch on two things though. The basics of the build command and [the tag flag](https://docs.docker.com/engine/reference/commandline/build/#tag-an-image--t).

First, what is the build command?

> The docker build command builds Docker images from a Dockerfile and a “context”. A build’s context is the set of files located in the specified PATH or URL. 

Next, what is the `-t` flag? It's how we set a [tag](https://docs.docker.com/engine/reference/commandline/tag/). In this case, I don't think the documentation is exactly clear to a new user.

> Create a tag TARGET_IMAGE that refers to SOURCE_IMAG

To paraphrase that, we're going to think of a tag as a human-readable name that refers to the Docker image we're going to create. We'll use the tag `testingtreeimage`, but you could also have called it anything you wanted. For my image that you can find on Docker Hub, I'm using the tag name `johnfmorton/tree-cli:latest`. Let's stick with `testingtreeimage` for now. (BTW, you can give multiple names to the same image. I can be both "James" and "Jimmy", if you want.)

### What is context?

Before we run the build command, we need to talk about "context". Let's see what [Understand build context](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#understand-build-context) from the docs says.

> When you issue a docker build command, the current working directory is called the build context. The Dockerfile is assumed to be located here by default, but you can specify a different location with the file flag (-f). Regardless of where the Dockerfile actually lives, all recursive contents of files and directories in the current directory are sent to the Docker daemon as the build context.

In the build command, we're about to issue, we use `.` to specify our context is the current directory we are issuing the build command from. We've also used the correct default name for our Dockerfile already, so no `-f` is needed.

Here's the command we'll run. It says "Docker, using the image function, build an image, tagging it with the word "testingtreeimage", using the "." context, i.e. this current directory, with the default file, i.e. Dockerfile."

```bash
docker image build -t testingtreeimage .
```
### About the TAG

Optionally, here's where we can use the `TAG` from the first line of the Dockerfile. We can pass in an alternative value for `TAG` when we build this image to use the 17-alpine node image instead of the 16-alpine node image we defined in our Dockerfile. 

```bash
docker image build --build-arg TAG=17-alpine -t testingtreeimage .
```

The default we have in the Dockerfile works fine in this case, but to be complete, I wanted to be sure to cover it here.
If everything goes according to plan in building your image, you should see something like the following.

```bash
docker image build -t testingtreeimage .
[+] Building 0.1s (7/7) FINISHED
 => [internal] load build definition from Dockerfile                                            0.0s
 => => transferring dockerfile: 37B                                                             0.0s
 => [internal] load .dockerignore                                                               0.0s
 => => transferring context: 2B                                                                 0.0s
 => [internal] load metadata for docker.io/library/node:16-alpine                               0.0s
 => [1/3] FROM docker.io/library/node:16-alpine                                                 0.0s
 => CACHED [2/3] WORKDIR /app                                                                   0.0s
 => CACHED [3/3] RUN set -eux;     npm install -g tree-cli                                      0.0s
 => exporting to image                                                                          0.0s
 => => exporting layers                                                                         0.0s
 => => writing image sha256:fcb052eeb981e7c30f7d95809c78f9b1840c3c67610d39990b5eac2f8b7c5e53    0.0s
 => => naming to docker.io/library/testingtreeimage                                             0.0s
```
### Listing Docker images

If you built the image successfully, you may wonder _where it's at_. We can list the Docker images we have on our machine, `docker image ls`, but if you've been using other images already, it's easier to spot this new image by using `| grep` and passing in a string which lets us narrow down the results. (We'll just use part of the tag, not the whole thing. Save your typing strength!)

```bash
docker image ls | grep 'testing'
```

You should see something like this:
```bash
docker image ls | grep 'testing'
testingtreeimage                          latest            fcb052eeb981   13 days ago    114MB
```
## Use your new image

Let's run a container based on our _testingtreeimage_. 

In the `container run` command we will use the `-v` flag. As you can see from the [documentation](https://docs.docker.com/engine/reference/commandline/container_run/#options), the `-v` option is to "bind mount a volume." Basically, we'll bind our current directory (using the environment variable $PWD, which print out our current working directory) to our container's `/app` directory. This means the `tree` command will see the files in our directory and list them. We're also using `--rm` to remove the container when we're done listing the files. 

Open up a new terminal window then run the following command. It will execute the default CMD we baked into the image inside this directory.

```bash
docker container run -v "$PWD":/app --rm testingtreeimage
```

You should see a list of the files in your current directory. Now try passing in a flag to show two levels of files. We must override the default command in our image and pass in `tree` plus the flag `-l 2`. 

```bash
docker container run -v "$PWD":/app --rm testingtreeimage tree -l 2
```

By the way, I've updated the `CMD` in the live Docker image at [johnfmorton/tree-cli](https://hub.docker.com/r/johnfmorton/tree-cli) to show 2 levels by default. Here's a link to that [commit](https://github.com/johnfmorton/docker-tree-cli/commit/4c21f8caee03814540928f748b799c9c0d508318) showing the update in Github.
## Where to go from here

Now that you know how to build a basic image, you can experiment with building your own custom images. Try to customize this one by changing the default CMD and rebuilding the image.  You can also explore tagging your image with your Dockerhub name push it to your own repo for the world to use.

If you've made it this far into the post and wondered about the boat design in the header, check out [the post on building it](https://shortypen.com/sailboats/squeeze-box-micro-sailboat/).

Good luck!

---

**Tags:** docker
