Docker Images & Tags

Docker & DevOps · Lesson 5 of 26 8 min read Updated

Every container starts from an image, so choosing the right image is one of the most important decisions you make with Docker. A good choice gives you a small, safe, predictable container. A careless choice, such as always using latest, can break your app on a random Tuesday when a new version appears.

In this lesson you will learn to read image names, pick tags, pull and list images, look inside them and clean them up.

What is inside an image?

An image is a read-only bundle of files plus some settings. The files are a small Linux user space (for example Alpine or Debian), a language runtime (Node.js, Python, PHP), libraries and your app. The settings include the default command, environment variables, the working directory and which user to run as.

An image is stored as a stack of layers. Each layer holds the file changes made by one build step. Images that share layers store them only once on disk, so ten images based on node:26-alpine do not take ten times the space. You will study layers closely in Layers & the Build Cache.

Reading an image name

A full image reference has up to four parts:

Text
ghcr.io/priya-sharma/medical-shop:2.1.0
└──┬──┘ └────┬─────┘ └─────┬────┘ └─┬─┘
registry   namespace   repository   tag
  • Registry: the server that stores the image. If you leave it out, Docker uses Docker Hub (docker.io).
  • Namespace: usually a user or organisation name. Official images on Docker Hub, such as nginx or mysql, live in a special namespace called library, which you never need to type.
  • Repository: the image's name.
  • Tag: a version label. If you leave it out, Docker uses the tag latest.

So nginx really means docker.io/library/nginx:latest, and mysql:8.4 means docker.io/library/mysql:8.4.

Tags and variants

Official images publish many tags for the same software. For Node.js, you will see tags like these:

TagMeaning
node:26Latest Node.js 26.x on a full Debian base with build tools (large)
node:26-slimNode.js 26.x on a trimmed Debian base (medium)
node:26-alpineNode.js 26.x on Alpine Linux (small)
node:26.1.0-alpineExactly version 26.1.0 on Alpine
node:latestWhatever the newest Node.js release is today

Alpine images are tiny because Alpine uses the small musl C library and BusyBox tools. A few packages with compiled parts behave differently on musl, so if something fails on Alpine, try the -slim Debian variant. Both are good choices; the full images are mainly useful when you need compilers during a build.

A tag can move: node:26-alpine points to a newer image every time Node.js 26 gets a patch. If you need an image that can never change, use its digest, a SHA-256 fingerprint of the image's content:

Dockerfile
FROM node:26-alpine@sha256:9a0b4c1d2e3f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b

Docker checks the content against the digest, so nobody can swap the image behind your back. Teams often let a bot such as Dependabot or Renovate update pinned digests through pull requests.

Pulling and listing images

docker pull downloads an image without running it. docker run pulls automatically, but pulling first is useful on servers, so the download does not happen while your app is down.

Terminal
docker pull node:26-alpine
Output
26-alpine: Pulling from library/node
2d35ebdb57d9: Pull complete
6e5a7b8c9d01: Pull complete
a3f1b2c4d5e6: Pull complete
7c8d9e0f1a2b: Pull complete
Digest: sha256:9a0b4c1d2e3f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b
Status: Downloaded newer image for node:26-alpine
docker.io/library/node:26-alpine

Each Pull complete line is one layer. The last line shows the full name that Docker resolved. Now list the images on your computer:

Terminal
docker image ls
Output
IMAGE                ID             DISK USAGE   CONTENT SIZE   EXTRA
alpine:3.22          4bcff63911fc       12.8MB         3.99MB   U
hello-world:latest   56433a6be3fd       20.3kB         3.96kB   U
mysql:8.4            1f0a3c9d5b6e       1.03GB          238MB
nginx:alpine         61e01287e546       75.4MB         21.9MB   U
node:26-alpine       9a0b4c1d2e3f        218MB         56.2MB
ubuntu:26.04         4f6c5a0b6c1e        113MB         30.6MB   U

This is the layout used since Docker 29. CONTENT SIZE is the compressed size you download. DISK USAGE is the space the image takes once unpacked. A U under EXTRA means at least one container (running or stopped) uses the image. The short form docker images does the same thing.

Inspecting an image

docker image inspect prints everything Docker knows about an image as JSON. The --format option picks out single fields using Go template syntax, which saves you scrolling:

Terminal
docker image inspect --format '{{.Os}}/{{.Architecture}}' node:26-alpine
docker image inspect --format '{{json .Config.Cmd}}' node:26-alpine
docker image inspect --format '{{json .Config.ExposedPorts}}' nginx:alpine
Output
linux/amd64
["node"]
{"80/tcp":{}}

The first answer shows the image's platform. The second shows the default command, which runs when you start the container without giving your own command. The third shows that the Nginx image documents port 80 as the port its server listens on.

Platforms: amd64 and arm64

Images contain machine code for one CPU family. Most servers and Windows laptops use amd64 (also called x86-64). Apple silicon Macs, Raspberry Pi boards and many cheaper cloud servers use arm64. Official images are multi-platform: one tag holds a version for each CPU, and Docker picks the one that matches your machine automatically.

You can ask for a different platform with --platform. This is useful when your Mac is arm64 but your server is amd64:

Terminal
docker pull --platform linux/amd64 nginx:alpine
docker run --rm --platform linux/arm64 alpine:3.22 uname -m
Output
aarch64

Running a foreign platform works through emulation (Docker Desktop includes it; on a Linux server you install QEMU support first), which is slow. It is fine for a quick test, not for daily use. When you build your own images for a different server, you can build multi-platform images with docker buildx build --platform linux/amd64,linux/arm64.

Removing images

Images take disk space, and old ones pile up after many builds and pulls.

Terminal
docker image rm ubuntu:26.04     # remove one image (same as docker rmi)
docker image prune               # remove dangling images (no tag left)
docker image prune -a            # remove every image no container uses
docker system df                 # see how much space Docker uses

Docker refuses to remove an image that a container still uses, even a stopped one. Remove the container first, or add -f if you are sure. Be careful with docker image prune -a on a server: the next deploy will have to download everything again.

Common mistakes

  • Using latest in production. Your build silently changes when a new release appears. Pin a version.
  • Choosing a huge image without thinking. node:26 is several times bigger than node:26-alpine. Bigger images download slower and contain more software that may need security fixes.
  • "exec format error" when a container starts. The image was built for a different CPU platform, for example an arm64 image built on a Mac and run on an amd64 server. Build for the right platform with --platform.
  • Trusting random images. Prefer Docker Official Images and images from verified publishers. An unknown image can contain anything.

Key points

  • An image reference is registry/namespace/repository:tag; Docker Hub and the latest tag are the defaults.
  • Pin a version tag such as node:26-alpine, or a digest for a reference that can never change.
  • Alpine variants are smallest; -slim Debian variants are a safe middle ground.
  • docker image ls lists images (Docker 29 shows DISK USAGE and CONTENT SIZE); docker image inspect --format reads single fields.
  • Multi-platform images hold builds for amd64 and arm64; --platform chooses one explicitly.
  • docker image rm, docker image prune and docker system df keep disk use under control.

Exercise

Pull python:3.15-slim and python:3.15-alpine. Compare their sizes with docker image ls. Then use docker image inspect --format to print the default command of each image. Finally, remove the larger one.

Show solution
Terminal
docker pull python:3.15-slim
docker pull python:3.15-alpine
docker image ls python
docker image inspect --format '{{json .Config.Cmd}}' python:3.15-slim
docker image inspect --format '{{json .Config.Cmd}}' python:3.15-alpine
docker image rm python:3.15-slim

Adding a repository name after docker image ls shows only that repository's tags. Both images start the Python interpreter by default (["python3"]), and the Alpine one is noticeably smaller, so the slim one is the larger image to remove.

Quick Quiz

1. What full reference does Docker use when you type nginx?

2. Why should you avoid the latest tag for deployments?

3. What does a digest such as @sha256:... guarantee?

4. A container fails with "exec format error" on your amd64 server. What is a likely cause?

Learn Docker & DevOps with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call