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.

