Skip to main content

Docker Image

A read-only, layered filesystem snapshot that serves as the template for creating containers. Each layer represents a Dockerfile instruction and is cached and reused across builds to speed up image creation.

Docker Image — The Blueprint for Every Container

What Is a Docker Image in Simple Terms?

A Docker image is a read-only package that contains everything needed to run an application — the code, the runtime, the libraries, the configuration files, and the operating system files. When you run an image, Docker creates a container from it. The image never changes — it is the template. The container is the running instance.

Think of an image like a cooking recipe. The recipe itself never changes — it tells you exactly what ingredients and steps to use. Every time you follow the recipe, you create a new meal (the container). Multiple chefs can follow the same recipe simultaneously to create multiple identical meals.

Bash
+------------------------------------------+
| Docker Image (read-only template) |
| ubuntu:22.04 base |
| + Node.js 20 runtime |
| + npm packages from package.json |
| + application source code |
| + nginx config |
+------------------------------------------+
|
| docker run (creates)
|
v
+------------------+ +------------------+ +------------------+
| Container 1 | | Container 2 | | Container 3 |
| Running instance | | Running instance | | Running instance |
| Has own RW layer | | Has own RW layer | | Has own RW layer |
+------------------+ +------------------+ +------------------+
All three share the same read-only image layers
No duplication on disk

How Images Are Layered — The Union Filesystem

A Docker image is not a single monolithic file. It is a stack of layers. Each layer represents the filesystem changes made by one Dockerfile instruction. Understanding this is the key to understanding build speed, image size, and storage efficiency.

Bash
+------------------------------------------+
| Layer 4: COPY . . | <- your source code
| Size: 2MB |
+------------------------------------------+
| Layer 3: RUN npm install | <- installed node_modules
| Size: 150MB |
+------------------------------------------+
| Layer 2: COPY package.json ./ | <- just the manifest
| Size: 4KB |
+------------------------------------------+
| Layer 1: FROM node:20-alpine | <- Node.js + Alpine Linux
| Size: 55MB |
+------------------------------------------+
Total image size: ~207MB
Each layer is identified by a SHA256 hash of its contents

Layers are immutable — once created, they never change. When Docker builds a new image, it only creates new layers for instructions that changed. Everything else is reused from the cache.

Bash
# See all layers of an image
docker history node:20-alpine
# IMAGE CREATED CREATED BY SIZE
# a84f9c2b1d3e 3 weeks ago CMD ["node"] 0B
# b72c8a9f4e1d 3 weeks ago ENTRYPOINT ["docker-entry 0B
# c91d8b3f2a5e 3 weeks ago COPY docker-entrypoint.sh 550B
# ... RUN apk add ... 40.2MB
# ... ENV NODE_VERSION=20.10.0 0B
# ... FROM alpine:3.18 7.34MB
# See your own image layers
docker history my-payment-api:latest
# This shows exactly how much each of your Dockerfile instructions added
# The layer with the largest size is your optimization target

OverlayFS — How Layers Stack Into a Filesystem

Docker uses OverlayFS (Overlay Filesystem) to merge multiple read-only layers into a single unified view. When you run a container, Docker adds one writable layer on top:

Bash
+------------------------------------------+
| Container Read-Write Layer | <- writes go here
| (deleted when container is removed) | unique per container
+==========================================+
| Image Layer 4: App source code | <- read-only
+==========================================+
| Image Layer 3: npm node_modules | <- read-only
+==========================================+
| Image Layer 2: package.json | <- read-only
+==========================================+
| Image Layer 1: node:20-alpine base | <- read-only
+==========================================+
|
| OverlayFS merges these
v
+------------------------------------------+
| Container sees one unified filesystem |
| /usr/bin/node exists (from Layer 1) |
| /app/package.json exists (from Layer 2) |
| /app/node_modules/ exists (from Layer 3) |
| /app/src/server.js exists (from Layer 4) |
+------------------------------------------+

When a container modifies a file that exists in a read-only layer, OverlayFS copies the file up to the writable layer and modifies it there — leaving the original layer untouched. This is called copy-on-write.

Image Naming and Tagging

Bash
# Full image reference format:
# [registry/][owner/]name[:tag][@digest]
# Docker Hub (default registry):
nginx # Short form: nginx:latest on Docker Hub
nginx:1.25 # Specific version
library/nginx:1.25 # Full form on Docker Hub
# Custom registry:
registry.razorpay.in/payment-api:v3.1.0
registry.razorpay.in/payment-api:v3.1.0-rc1
registry.razorpay.in/payment-api:latest # Never use in production!
# Digest reference (immutable — always the exact same image):
nginx@sha256:a84f9c2b1d3e...
# Tag an image
docker tag my-image:latest registry.razorpay.in/payment-api:v3.1.0
# Push to registry
docker push registry.razorpay.in/payment-api:v3.1.0
# Pull from registry
docker pull registry.razorpay.in/payment-api:v3.1.0

Working With Images

Bash
# List images on your local machine
docker images
# REPOSITORY TAG SIZE
# registry.razorpay.in/payment-api v3.1.0 207MB
# nginx 1.25 187MB
# node 20-alpine 55MB
# List images with digest
docker images --digests
# Shows SHA256 digest alongside each image
# Inspect image metadata
docker image inspect nginx:1.25
# Returns full JSON with: layers, exposed ports, environment, labels
# See what layers make up the image and their sizes
docker history nginx:1.25
# Pull an image without running it
docker pull node:20-alpine
# Remove an image from local storage
docker rmi nginx:1.24
# Cannot remove if a container (even stopped) is using the image
# Remove all dangling images (untagged, from old builds)
docker image prune
# Remove all unused images (not referenced by any container)
docker image prune -a
# Find dangling images (untagged = old build artifacts)
docker images -f dangling=true
# These accumulate on build machines and waste disk space

Image ID vs Image Digest

Bash
# Image ID: a short hash of the image configuration
# Can be different on different machines for the same image (rare but possible)
docker images --format '{{.ID}} {{.Repository}}:{{.Tag}}'
# a84f9c2b1d3e registry.razorpay.in/payment-api:v3.1.0
# Image Digest: SHA256 hash of the image manifest
# Globally unique and immutable — the exact same image bytes anywhere
docker images --digests | grep payment-api
# registry.razorpay.in/payment-api v3.1.0 sha256:b72c8a9f4e1d... 207MB
# Pull by digest for reproducible builds:
docker pull nginx@sha256:a84f9c2b1d3e...
# This ALWAYS gets exactly the same image
# Even if someone pushed a new nginx:1.25 after your last pull

Dangling Images — Cleaning Up Build Artifacts

Every time you rebuild an image with the same tag, the old image loses its tag and becomes dangling — it still exists on disk but has no name.

Bash
# Dangling images look like this:
docker images
# REPOSITORY TAG IMAGE ID CREATED SIZE
# <none> <none> a84f9c2b1d3e 2 hours ago 207MB <- dangling
# <none> <none> b72c8a9f4e1d 1 day ago 198MB <- dangling
# payment-api v3.1.0 c91d8b3f2a5e 3 minutes ago 207MB <- current
# Remove all dangling images
docker image prune -f
# On build machines, schedule this as a cron job
# otherwise disk fills up with hundreds of old image layers
# crontab entry: 0 3 * * * docker image prune -f

Troubleshooting Reference

Problem Cause Fix
docker: Error response from daemon: pull access denied Not logged in to private registry docker login registry.razorpay.in
Error: No such image Image not pulled locally docker pull image:tag
Cannot remove image: image is being used by stopped container Stopped container references image docker rm container-id then docker rmi image
Disk filling up on build machine Dangling images accumulating docker image prune -a -f
Different machines get different images for same tag Tag was overwritten Pin by digest: image@sha256:...
Tip

Run docker history your-image:tag --no-trunc to see the full Dockerfile instruction that created each layer. This is the fastest way to understand what is taking up space in a large image — each layer shows its size and the exact command that created it.

Remember

Image layers are shared between images on the same host. If you have 10 containers all running from node:20-alpine, the Alpine base layers are stored once on disk — not 10 times. This storage efficiency is a major reason container images are so much more practical than VMs for running many services on the same host.

Common Mistake

Using the :latest tag in production. The latest tag is just a convention — it means whatever was last pushed with that tag. If someone pushes a broken build and tags it latest, every deployment that pulls latest will get the broken build. Always tag images with an immutable identifier like a git SHA or a semantic version.

Security

Base images inherit CVEs (known vulnerabilities) from the operating system packages they include. An ubuntu:22.04 base typically has 200+ known CVEs. An alpine:3.18 base has 0-5. Always scan your images with Trivy (trivy image your-image:tag) before pushing to production and fail CI if CRITICAL severity CVEs are found.

Frequently Asked Questions

Why are Docker images built from layers instead of as one single filesystem blob?

Layering lets Docker cache and reuse individual filesystem diffs across builds and across images — if two images both start `FROM node:20` and run the same `npm install` early on, they can share those layers on disk and during registry pulls, saving both storage and bandwidth. Each Dockerfile instruction that changes the filesystem (RUN, COPY, ADD) produces one immutable layer, and the union filesystem stacks them at runtime to present a single merged view to the container process.

What's the classic image-size mistake that layering makes easy to fall into?

Installing build dependencies and cleaning them up in separate RUN instructions — because each instruction becomes its own immutable layer, deleting a file in a later layer doesn't shrink the image; the earlier layer still contains the full file, just hidden. The fix is combining install-and-cleanup into a single RUN command (or better, using multi-stage builds so build tools never end up in the final image layer at all).