SuperGeekery: A blog probably of interest only to nerds by John F Morton.

A blog prob­a­bly of inter­est only to nerds by John Morton.

04Feb2022

Build a basic Dock­er image

Make a boat
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 dis­sect a small Dock­er image I have on Dock­er Hub: johnf­mor­ton/tree-cli.

It’s not a com­plex image, but its sim­plic­i­ty is good because it is fair­ly easy to under­stand. Once you under­stand the basics, you can cre­ate your own more com­plex Dock­er images.

The tree-cli image is based on an offi­cial Node image and has one cus­tomiza­tion: I’ve installed tree-cli from npm, which is a pack­age that allows us to list files in a tree struc­ture. For exam­ple, direc­to­ry list­ings look like this when using tree-cli.

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

directory: 1 file: 4

It’s so sim­ple, you may won­der why a Dock­er image would need to be cre­at­ed at all!

If you want­ed to install tree-cli on your own machine, you could use npm and install it local­ly from the com­mand line and it would prob­a­bly work just fine.

npm install -g tree-cli

I’m not doing that though because I’m try­ing to min­i­mize apps I install direct­ly on my com­put­er. For the full sto­ry on this method of not installing things, read Dock Life, a post by Andrew Welch. It’s good stuff. You’ll see the tree com­mand men­tioned in there. 

The Dock­er Hub page vs the Github repo page

If you took at the Dock­er Hub list­ing for johnf­mor­ton/tree-cli, you will find the actu­al image, but the thing you’re miss­ing is the Dock­er­file which is the doc­u­ment that I used to cre­ate the image. The clos­est you get to that is look­ing at the lay­ers of the image.

Docker hub image layers

A screenshot of the layers from Docker Hub for tree-cli

The lay­ers have a lot of help­ful infor­ma­tion but look­ing at the Dock­er­file is much eas­i­er to under­stand. I’ve post­ed the whole project, includ­ing the Dock­er­file, at Github, https://​github​.com/​j​o​h​n​f​m​o​r​t​o​n​/​d​o​c​k​e​r​-​t​r​e​e-cli.

Let’s take a look at the Dock­er­file as a whole and then review it line-by-line. 

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"]

Review­ing the Dockerfile

I told you it was short. 

Let’s start at the very begin­ning 🎼 and review the instruc­tions on each line of the Dock­er­file in order to be explic­it about what’s hap­pen­ing here.

ARG line

ARG TAG=16-alpine

ARG is an argu­ment, i.e. a vari­able we’re cre­at­ing with the name TAG. You might have guessed that. The Dock­er doc­u­men­ta­tion says the ARG instruc­tion defines a vari­able that users can pass at build-time to the builder with the dock­er build com­mand using the --build-arg <varname>=<value> flag.”

In oth­er words, we’re going to cre­ate an argu­ment, TAG, that may be use­ful lat­er when we build this image. If you don’t use it, the vari­able will con­tain the string 16-alpine” as the default.

FROM line

FROM node:$TAG

FROM, accord­ing to the docs, sets the Base Image for sub­se­quent instruc­tions.” The base image is our start­ing point. There are many base images, offi­cial and unof­fi­cial, avail­able on Dock­er Hub. Offi­cial images are, accord­ing to the docs, a curat­ed set of Dock­er repos­i­to­ries host­ed on Dock­er Hub”. Check out the offi­cial base image page at Dock­er Hub and you’ll see many options: Ubun­tu Lin­ux, python, MySQL, Nginx, alpine, and more. You can con­fi­dent­ly use these images because Dock­er, Inc. spon­sors a ded­i­cat­ed team that is respon­si­ble for review­ing and pub­lish­ing all con­tent in the Dock­er Offi­cial Images.” (link)

There are even more unof­fi­cial images as well. For exam­ple, the tree-cli image we’re going over is an unof­fi­cial” image. 

We need a node envi­ron­ment to run tree-cli because it’s a node mod­ule, and we’re going to use an offi­cial Node base image called 16-alpine”, which includes node ver­sion 16. I like that it’s an offi­cial image. The term alpine”, when it comes to Dock­er, means that it’s a small, min­i­mal image, based on Lin­ux, secu­ri­ty, sim­plic­i­ty and resource effi­cien­cy”.

What if you want­ed to use 17-alpine” instead? We’ll get to that when we build the actu­al image. (Spoil­er alert: we’ll use the TAG men­tioned earlier.)

WORKDIR line

The WORKDIR, as the docs say, sets the work­ing direc­to­ry for any RUN, CMD, ENTRY­POINT, COPY and ADD instruc­tions that fol­low it in the Dockerfile.”

The line in our Dock­er­file, WORKDIR /app, means we are set­ting the direc­to­ry /app as the base direc­to­ry from which the rest of the com­mands we issue will be run. To be clear, this is ins­de the con­tain­er that a direc­to­ry called /app will exist.

RUN line

Let’s refer to the doc­u­men­ta­tion again. The RUN instruc­tion will exe­cute any com­mands in a new lay­er on top of the cur­rent image and com­mit the results.” That means we’re going to run a com­mand as we’re build­ing this Dock­er image. This will cus­tomize the base image by doing some­thing 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 ref­er­ence the Dock­er doc­u­men­ta­tion for the set com­mand. We’re going to ref­er­ence doc­u­men­ta­tion for bash for set.

Con­fused as to why we’re not ref­er­enc­ing Dock­er doc­u­men­ta­tion? At this point in the process, imag­ine you’ve got your ter­mi­nal open in this brand new, clean Lin­ux machine. You’re at the bash com­mand line so that’s our cur­rent frame of ref­er­ence. Let’s see what the bash doc­u­men­ta­tion says about set.

set allows you to change the val­ues of shell options and set the posi­tion­al para­me­ters, or to dis­play the names and val­ues of shell variables.

So we’re going to set some options for the bash com­mand (i.e. an npm install com­mand). We’re set­ting 3 options, e, u, and r. We’re com­bin­ing them all togeth­er with -eur;. Let’s see what each option does.

  • -e : exit imme­di­ate­ly if a com­mand returns a non-zero status
  • -u : treat unset vari­ables and para­me­ters oth­er than the spe­cial para­me­ters ‘@’ or ‘*’ as an error 
  • -x : print a trace of sim­ple commands

In short, we’re try­ing to pre­vent errors and print out as much help­ful info to the ter­mi­nal as we can when we exe­cute our command.

Final­ly, we’ve end­ed this line with the ; and then \. The semi­colon is just basi­cal­ly end­ing the set com­mand. The back­slash is a sort of new line” com­mand that let’s us exe­cute addi­tion­al com­mands with­out cre­at­ing a new lay­er in the Dock­er image. . There are more best prac­tices like this in the Dock­er docs, but basi­cal­ly, we’re doing this to keep our Dock­er image as small as possible.

The npm install ‑g tree-cli’ line

We’re final­ly installing some­thing on our Dock­er image. Since we’re being ver­bose here though, let’s talk about how we knew to use this exact com­mand. We’re going to look at the npm page for tree-cli, specif­i­cal­ly where it men­tions installing the software. 

npm install -g tree-cli

Com­ment lines in the Dock­er­file + CMD line

I’m going to treat the rest of the file as a sin­gle line. The lines that begin with a # are com­ments. They are here for ref­er­ence only, so let’s talk about the CMD com­mand since it’s the next active line in the Dock­er­file. Let’s go back again to the Dock­er­file doc­u­men­ta­tion and see what it says.

If you vis­it that link, there is quite a bit of infor­ma­tion there. Here are the impor­tant bits for our image.

The main pur­pose of a CMD is to pro­vide defaults for an exe­cut­ing container.

I hear you! What do those words mean?

Let’s imag­ine our­selves just a few min­utes into the future when you’ve got a com­plet­ed Dock­er image that does just one thing: it can list out the direc­to­ry using the tree com­mand because it has tree-cli installed on it. That’s basi­cal­ly all it can do.

So, what do we want the default com­mand to be when it’s run? I think we should run the tree” com­mand. The CMD lets us set the default com­mand to run if noth­ing else is spec­i­fied when a user tries to use our Dock­er image.

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

If you’re won­der­ing why it says /usr/local/bin/ before tree, think about the npm install process. The npm install com­mand installs pro­grams into the /usr/local/bin direc­to­ry, so we’re just point­ing to where tree lives on our lit­tle itty bit­ty Lin­ux box.

Option­al­ly, as you can see from my com­ments in the Dock­er­file, we could have default­ed to just show­ing a bash prompt. I chose not to do that but left the com­ment tag in because it was use­ful ref­er­ence for me when I look at the file.

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

Build­ing the image locally

In case you’re play­ing along on your com­put­er, let’s make sure we’ve got every­thing aligned. 

A direc­to­ry on your com­put­er con­tains the Dockerfile we’ve been work­ing on above. The file­name is just Dockerfile. It’s not Dockerfile.txt or dockerfile or any­thing else. From with­in the ter­mi­nal, you have nav­i­gat­ed into that direc­to­ry. You can con­firm this by typ­ing the ls com­mand and you’ll see the Dockerfile in there. Good?

We will next use use the build com­mand. Here is the URL for the doc­u­men­ta­tion on the build com­mand. There is a lot of infor­ma­tion there. We’re only going to touch on two things though. The basics of the build com­mand and the tag flag.

First, what is the build command?

The dock­er build com­mand builds Dock­er images from a Dock­er­file and a con­text”. A build’s con­text is the set of files locat­ed in the spec­i­fied PATH or URL

Next, what is the -t flag? It’s how we set a tag. In this case, I don’t think the doc­u­men­ta­tion is exact­ly clear to a new user.

Cre­ate a tag TARGET_IMAGE that refers to SOURCE_IMAG

To para­phrase that, we’re going to think of a tag as a human-read­able name that refers to the Dock­er image we’re going to cre­ate. We’ll use the tag testingtreeimage, but you could also have called it any­thing you want­ed. For my image that you can find on Dock­er Hub, I’m using the tag name johnfmorton/tree-cli:latest. Let’s stick with testingtreeimage for now. (BTW, you can give mul­ti­ple names to the same image. I can be both James” and Jim­my”, if you want.)

What is context?

Before we run the build com­mand, we need to talk about con­text”. Let’s see what Under­stand build con­text from the docs says.

When you issue a dock­er build com­mand, the cur­rent work­ing direc­to­ry is called the build con­text. The Dock­er­file is assumed to be locat­ed here by default, but you can spec­i­fy a dif­fer­ent loca­tion with the file flag (-f). Regard­less of where the Dock­er­file actu­al­ly lives, all recur­sive con­tents of files and direc­to­ries in the cur­rent direc­to­ry are sent to the Dock­er dae­mon as the build context.

In the build com­mand, we’re about to issue, we use . to spec­i­fy our con­text is the cur­rent direc­to­ry we are issu­ing the build com­mand from. We’ve also used the cor­rect default name for our Dock­er­file already, so no -f is needed.

Here’s the com­mand we’ll run. It says Dock­er, using the image func­tion, build an image, tag­ging it with the word test­ingtreeim­age”, using the “.” con­text, i.e. this cur­rent direc­to­ry, with the default file, i.e. Dockerfile.”

docker image build -t testingtreeimage .

About the TAG

Option­al­ly, here’s where we can use the TAG from the first line of the Dock­er­file. We can pass in an alter­na­tive val­ue 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. 

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

The default we have in the Dock­er­file works fine in this case, but to be com­plete, I want­ed to be sure to cov­er it here.

If every­thing goes accord­ing to plan in build­ing your image, you should see some­thing like the following.

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

List­ing Dock­er images

If you built the image suc­cess­ful­ly, you may won­der where it’s at. We can list the Dock­er images we have on our machine, docker image ls, but if you’ve been using oth­er images already, it’s eas­i­er to spot this new image by using | grep and pass­ing in a string which lets us nar­row down the results. (We’ll just use part of the tag, not the whole thing. Save your typ­ing strength!)

docker image ls | grep 'testing'

You should see some­thing like this:

docker image ls | grep 'testing'
testingtreeimage                          latest            fcb052eeb981   13 days ago    114MB

Use your new image

Let’s run a con­tain­er based on our test­ingtreeim­age.

In the container run com­mand we will use the -v flag. As you can see from the doc­u­men­ta­tion, the -v option is to bind mount a vol­ume.” Basi­cal­ly, we’ll bind our cur­rent direc­to­ry (using the envi­ron­ment vari­able $PWD, which print out our cur­rent work­ing direc­to­ry) to our con­tain­er’s /app direc­to­ry. This means the tree com­mand will see the files in our direc­to­ry and list them. We’re also using --rm to remove the con­tain­er when we’re done list­ing the files. 

Open up a new ter­mi­nal win­dow then run the fol­low­ing com­mand. It will exe­cute the default CMD we baked into the image inside this directory.

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

You should see a list of the files in your cur­rent direc­to­ry. Now try pass­ing in a flag to show two lev­els of files. We must over­ride the default com­mand in our image and pass in tree plus the flag -l 2.

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

By the way, I’ve updat­ed the CMD in the live Dock­er image at johnf­mor­ton/tree-cli to show 2 lev­els by default. Here’s a link to that com­mit show­ing the update in Github.

Where to go from here

Now that you know how to build a basic image, you can exper­i­ment with build­ing your own cus­tom images. Try to cus­tomize this one by chang­ing the default CMD and rebuild­ing the image. You can also explore tag­ging your image with your Dock­er­hub name push it to your own repo for the world to use.

If you’ve made it this far into the post and won­dered about the boat design in the head­er, check out the post on build­ing it.

Good luck!