Docker · Lesson 2 of 11

Images, Layers and Registries

Understand Docker image layers, tags and digests, then pull, tag and push images to Docker Hub and GitHub Container Registry the right way.

  • Beginner
  • 15 min read
  • 4 objectives

Before this lessonLesson 1: Containers and Docker Basics

What you will learn

  • Explain how images are built from read-only layers
  • Read image names: registry, repository, tag and digest
  • Tag and push an image to a registry
  • Clean up unused images safely

Your Progress

0 of 11 lessons 0%

  • Lessons0 / 11
  • Completed0
  • Est. time left~ 3 hours

Create a free account to keep your progress on every device.

Tip: pressing Next marks this lesson complete automatically.

In the first lesson you ran nginx and hello-world without thinking much about where they came from. Every container starts from an image, and every image lives somewhere: on your machine, on Docker Hub, or in a private registry your company runs. Knowing how images are stored and named saves you from slow pulls, surprise upgrades and bloated disks.

This lesson looks inside an image, explains what a tag really is, and walks through pushing your own image to a registry so a server (or a teammate) can pull it.

An image is a stack of layers

An image is not one big file. It is an ordered stack of read-only layers, each one a set of filesystem changes (files added, changed or deleted). When you start a container, Docker adds one thin writable layer on top. Everything the container writes goes into that top layer, which is thrown away when the container is removed.

Layers are shared. If ten images are all based on python:3.12-slim, the base layers are stored once on disk and downloaded once. You can see the layers of any image with docker history:

docker pull python:3.12-slim
docker history python:3.12-slim
Output
IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
a1f3c2e9b7d4   2 weeks ago   CMD ["python3"]                                 0B        buildkit.dockerfile.v0
<missing>      2 weeks ago   RUN /bin/sh -c set -eux; for src in idle3 p...  36B       buildkit.dockerfile.v0
<missing>      2 weeks ago   RUN /bin/sh -c set -eux; savedAptMark="$(a...  41.2MB    buildkit.dockerfile.v0
<missing>      2 weeks ago   ENV PYTHON_VERSION=3.12.11                      0B        buildkit.dockerfile.v0
<missing>      2 weeks ago   RUN /bin/sh -c set -eux; apt-get update; a...  9.4MB     buildkit.dockerfile.v0
<missing>      2 weeks ago   ENV LANG=C.UTF-8                                0B        buildkit.dockerfile.v0
<missing>      2 weeks ago   # debian.sh --arch 'arm64' out/ 'bookworm' ...  97.2MB    debuerreotype 0.15

Read it bottom to top: a Debian base, some system packages, then Python itself. Instructions like ENV and CMD only change metadata, so they show 0B. The <missing> IDs are normal: intermediate layers pulled from a registry do not have local image IDs.

Anatomy of an image name

A full image reference has up to four parts:

ghcr.io/stackcone/api:1.4.2@sha256:9c1e...
|______| |_______| |_| |___| |__________|
registry namespace repo  tag     digest
  • Registry: the server that stores the image. If omitted, Docker assumes Docker Hub (docker.io).
  • Namespace/repository: the user or organisation and the image name. Official images like nginx are really docker.io/library/nginx.
  • Tag: a human-friendly label. If omitted, Docker uses latest.
  • Digest: a SHA-256 hash of the image manifest. It identifies exactly one image and never changes.

Tags move, digests do not

A tag is just a pointer, like a Git branch. The maintainers of postgres:16 move that tag every time they publish a patch release. That is convenient, but it means postgres:16 today and postgres:16 next month can be different images. The tag latest is the worst offender: it means "whatever was pushed last", not "the newest stable version".

To see the digest behind a tag:

docker images --digests postgres
Output
REPOSITORY   TAG       DIGEST                                                                    IMAGE ID       CREATED       SIZE
postgres     16        sha256:4f8b3e1c2a7d9e0f5b6c8a1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f   3c7a9e2b1f40   10 days ago   438MB

Multi-platform images

One tag can point to several images, one per CPU architecture. When you pull python:3.12-slim on an Apple Silicon Mac you get the linux/arm64 variant; on a typical cloud VM you get linux/amd64. This list is called a manifest list (or image index):

docker buildx imagetools inspect python:3.12-slim
Output
Name:      docker.io/library/python:3.12-slim
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:2b0079146a74e23bf4ae8f6a28e1b484c6292f6fb904cbb51825b4a19812fcd8

Manifests:
  Name:      docker.io/library/python:3.12-slim@sha256:6d1e...
  Platform:  linux/amd64

  Name:      docker.io/library/python:3.12-slim@sha256:e2a4...
  Platform:  linux/arm64/v8
  ...

This matters when you build on a Mac and deploy to an amd64 server. An image built only for arm64 will fail with exec format error on amd64. The CI lesson later in this course shows how to build both at once.

Tagging and pushing your own image

To share an image you give it a name that includes the target registry, log in, and push. Here is the flow for GitHub Container Registry (GHCR), which is free for public images and works well with GitHub Actions:

# build with a local name
docker build -t stackcone-api:1.0.0 .

# add a second name that points at the registry
docker tag stackcone-api:1.0.0 ghcr.io/stackcone/api:1.0.0

# log in (you will be prompted for a personal access token)
docker login ghcr.io -u ada

# upload
docker push ghcr.io/stackcone/api:1.0.0
Output
The push refers to repository [ghcr.io/stackcone/api]
5f70bf18a086: Pushed
8d2e1b9a4c33: Pushed
1c3f7e5d9b21: Mounted from library/python
0a9b8c7d6e5f: Mounted from library/python
1.0.0: digest: sha256:9c1e4b7a2d3f5e6a8b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a size: 1789

Notice that only your own layers were uploaded. The Python base layers were already known to the registry, so they were "mounted" instead of re-sent. The final line gives you the digest you can pin in deployments. docker tag never copies data; it just adds another name to the same image ID.

For Docker Hub the name is simply youruser/api:1.0.0. For AWS ECR, Google Artifact Registry or Azure ACR, the registry part is a long hostname and you log in with the cloud CLI, but the tag and push steps are identical.

Keeping your disk under control

Images pile up quickly, especially dangling images (old builds that lost their tag when you rebuilt). Check what is using space, then prune:

docker system df
docker image prune        # remove dangling images
docker image prune -a     # remove every image not used by a container
Output
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          23        4         6.12GB    4.87GB (79%)
Containers      5         2         48.3MB    12.1MB (25%)
Local Volumes   6         3         1.02GB    310MB (30%)
Build Cache     112       0         2.4GB     2.4GB

Recap

  • An image is a stack of shared, read-only layers; a container adds one writable layer on top.
  • A reference is registry/namespace/repo:tag@digest; the default registry is Docker Hub and the default tag is latest.
  • Tags are movable pointers; digests identify one exact image forever.
  • Share images with docker tag, docker login and docker push; unchanged base layers are not re-uploaded.
  • Use docker system df and docker image prune to reclaim disk space.
# Write your solution here

Finished reading? Mark this lesson complete to track your progress.

Up next · Lesson 3Writing a DockerfileBuild your own image, use layer caching and keep images small.