Working Effectively with Docker Images

Introduction

In this article, I am going to begin by practicing on Docker images. I will be using my newfound knowledge of Docker images to pull, build, and launch containers.

Worth pinning down the vocabulary first, because the two words get used interchangeably and they are not the same thing. An image is the read-only template: filesystem, plus the metadata saying what to run. A container is a running instance of one, with a thin writable layer on top. One image, as many containers as you like, and nothing a container writes ever changes the image it came from.

Begin by logging in to the server using the credentials provided on AWS ec2 instance:

$ ssh cloud_user@PUBLIC_IP_ADDRESS

Pull the latest Image

Pull the latest alpine image from Docker Hub.

$ docker image pull alpine:latest
latest: Pulling from library/alpine
df20fa9351a1: Pull complete
Digest: sha256:185518070891758909c9f839cf4ca393ee977ac378609f700f60a771a2dfe321
Status: Downloaded newer image for alpine:latest
docker.io/library/alpine:latest

One line of Pull complete, because alpine is a single layer. That will look different in a moment.

The digest underneath is the interesting part. That sha256 identifies this exact image content, permanently. Tags move around, digests do not, which is why anything that genuinely has to be reproducible gets pinned by digest rather than by tag.

You can confirm it is there with:

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
alpine              latest              a24bb4013296        3 months ago        5.57MB

5.57MB. Keep that number in mind for the comparison further down, because it is most of the reason people build on alpine in the first place.

Pull the latest httpd image

I will pull the latest httpd image from the following command.

$ docker pull httpd:latest
latest: Pulling from library/httpd
bf5952930446: Pull complete
3d3fecf6569b: Pull complete
b5fc3125d912: Pull complete
679d69c01e90: Pull complete
76291586768e: Pull complete
Digest: sha256:3cbdff4bc16681541885ccf1524a532afa28d2a6578ab7c2d5154a7abc182379
Status: Downloaded newer image for httpd:latest
docker.io/library/httpd:latest

Five layers this time, each pulled separately. That is not just detail, it is the mechanism: layers are cached and shared, so an image built on the same base as one you already have only downloads the parts that differ. It is why the second pull of a related image is so much quicker than the first.

Note: If you don’t put the version, the latest version is assumed.

That note deserves more than it usually gets. The tag latest is a label like any other, not a promise of freshness. It points at whatever the maintainer last tagged as latest, it changes without warning, and a build that worked this morning can produce something different this afternoon. Pinning a version is the fix, which is exactly what happens next.

Pull nginx 1.15

I am going to pull nginx version 1.15 from the below mentioned command.

$ docker pull nginx:1.15
1.15: Pulling from library/nginx
743f2d6c1f65: Pull complete
6bfc4ec4420a: Pull complete
688a776db95f: Pull complete
Digest: sha256:23b4dcdf0d34d4a129755fc6f52e1c6e23bb34ea011b315d87e193033bcd1b68
Status: Downloaded newer image for nginx:1.15
docker.io/library/nginx:1.15

To confirm that it is there:

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
httpd               latest              a6ea92c35c43        4 weeks ago         166MB
alpine              latest              a24bb4013296        3 months ago        5.57MB
nginx               1.15                53f3fd8007f7        16 months ago       109MB

There is the comparison. 166MB for httpd, 109MB for nginx, 5.57MB for alpine. The two web servers carry a full Debian userland underneath them, and alpine does not.

Size is not only a download cost either. Every package in an image is something that can carry a vulnerability and something you may end up patching, so a smaller base is a smaller surface as well as a smaller file.

Compare the history

Look at the history for both the httpd and nginx images.

$ docker history httpd
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
a6ea92c35c43        4 weeks ago         /bin/sh -c #(nop)  CMD ["httpd-foreground"]     0B
<missing>           4 weeks ago         /bin/sh -c #(nop)  EXPOSE 80                    0B
<missing>           4 weeks ago         /bin/sh -c #(nop) COPY file:c432ff61c4993ecd…   138B
<missing>           4 weeks ago         /bin/sh -c #(nop)  STOPSIGNAL SIGWINCH          0B
<missing>           4 weeks ago         /bin/sh -c set -eux;   savedAptMark="$(apt-m…   61MB
<missing>           4 weeks ago         /bin/sh -c #(nop)  ENV HTTPD_PATCHES=           0B
<missing>           4 weeks ago         /bin/sh -c #(nop)  ENV HTTPD_SHA256=740eddf6…   0B
<missing>           4 weeks ago         /bin/sh -c #(nop)  ENV HTTPD_VERSION=2.4.46     0B
<missing>           4 weeks ago         /bin/sh -c set -eux;  apt-get update;  apt-g…   35.4MB
<missing>           4 weeks ago         /bin/sh -c #(nop) WORKDIR /usr/local/apache2    0B
<missing>           4 weeks ago         /bin/sh -c mkdir -p "$HTTPD_PREFIX"  && chow…   0B
<missing>           4 weeks ago         /bin/sh -c #(nop)  ENV PATH=/usr/local/apach…   0B
<missing>           4 weeks ago         /bin/sh -c #(nop)  ENV HTTPD_PREFIX=/usr/loc…   0B
<missing>           4 weeks ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B
<missing>           4 weeks ago         /bin/sh -c #(nop) ADD file:3af3091e7d2bb40bc…   69.2MB

This is the Dockerfile read back to you, bottom to top. The 69.2MB entry at the bottom is the Debian base being added, then packages go on, then the httpd build, then a series of 0B entries.

Those zero-byte lines are metadata rather than content. ENV, EXPOSE, CMD and WORKDIR record intent without writing a file, so they add a layer and no size. The #(nop) marker means exactly that: no operation on the filesystem.

The word missing in the IMAGE column throws people. Nothing is wrong. Only the final layer of a pulled image keeps an ID locally, because the intermediate ones were never built on this machine. Build an image yourself and those IDs are populated.

Note the following command gives an error because the latest version is assumed:

$ docker history nginx
Error response from daemon: No such image: nginx:latest

Worth reading that error properly, because it is more useful than it looks. You have nginx:1.15 locally and asked about nginx, so Docker filled in the tag latest, and no image by that name exists on the machine. Same reason a docker run with the wrong tag suddenly starts downloading something.

When you specify the right version, the command succeeds:

$ docker history nginx:1.15
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
53f3fd8007f7        16 months ago       /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon…   0B
<missing>           16 months ago       /bin/sh -c #(nop)  STOPSIGNAL SIGTERM           0B
<missing>           16 months ago       /bin/sh -c #(nop)  EXPOSE 80                    0B
<missing>           16 months ago       /bin/sh -c ln -sf /dev/stdout /var/log/nginx…   22B
<missing>           16 months ago       /bin/sh -c set -x  && apt-get update  && apt…   54.1MB
<missing>           16 months ago       /bin/sh -c #(nop)  ENV NJS_VERSION=1.15.12.0…   0B
<missing>           16 months ago       /bin/sh -c #(nop)  ENV NGINX_VERSION=1.15.12…   0B
<missing>           16 months ago       /bin/sh -c #(nop)  LABEL maintainer=NGINX Do…   0B
<missing>           16 months ago       /bin/sh -c #(nop)  CMD ["bash"]                 0B
<missing>           16 months ago       /bin/sh -c #(nop) ADD file:fcb9328ea4c115670…   55.3MB

Same shape, fewer layers. One thing worth spotting is the symlink from the nginx access log to /dev/stdout, 22 bytes, right there in the history. That is the standard container logging trick: write to standard output and let the runtime collect it, rather than writing log files inside a container that will be thrown away.

Conclusion

Basically, that’s all you need to know about working with Docker images. There are more tutorials that will go into deeper explanation of using Docker thoroughly, but this is a simple article to just get familiar with images.

The three things worth carrying forward: images are layers and layers get cached, latest is a moving target so pin what matters, and docker history will tell you what is actually inside something before you build on it.

Need the daemon itself first? Installing Docker on Ubuntu 20.04 covers the repository setup and the docker group decision that comes with it.

Avatar photo

Asif Khan

I have spent over 10 years working across IT systems, open source software, DevOps, Linux administration and cloud operations. Three things drive most of what I do: automation, security and resilience. Much of that work involves planning and building the platforms that sit behind services people rely on daily, which means designing for failure just as carefully as for load. Cloud computing held my attention early on, largely for its flexibility. Being able to scale up and then back down again means far less guessing about how much capacity you will need. Across projects I work with the full DevOps toolchain, from provisioning, orchestration and configuration management through to release management and microservices architecture.