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.
+------------------------------------------+| 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 diskHow 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.
+------------------------------------------+| 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: ~207MBEach layer is identified by a SHA256 hash of its contentsLayers 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.
# See all layers of an imagedocker 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 layersdocker 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 targetOverlayFS — 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:
+------------------------------------------+| 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
# Full image reference format:# [registry/][owner/]name[:tag][@digest] # Docker Hub (default registry):nginx # Short form: nginx:latest on Docker Hubnginx:1.25 # Specific versionlibrary/nginx:1.25 # Full form on Docker Hub # Custom registry:registry.razorpay.in/payment-api:v3.1.0registry.razorpay.in/payment-api:v3.1.0-rc1registry.razorpay.in/payment-api:latest # Never use in production! # Digest reference (immutable — always the exact same image):nginx@sha256:a84f9c2b1d3e... # Tag an imagedocker tag my-image:latest registry.razorpay.in/payment-api:v3.1.0 # Push to registrydocker push registry.razorpay.in/payment-api:v3.1.0 # Pull from registrydocker pull registry.razorpay.in/payment-api:v3.1.0Working With Images
# List images on your local machinedocker images# REPOSITORY TAG SIZE# registry.razorpay.in/payment-api v3.1.0 207MB# nginx 1.25 187MB# node 20-alpine 55MB # List images with digestdocker images --digests# Shows SHA256 digest alongside each image # Inspect image metadatadocker image inspect nginx:1.25# Returns full JSON with: layers, exposed ports, environment, labels # See what layers make up the image and their sizesdocker history nginx:1.25 # Pull an image without running itdocker pull node:20-alpine # Remove an image from local storagedocker 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 spaceImage ID vs Image Digest
# 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 anywheredocker 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 pullDangling 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.
# 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 imagesdocker 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 -fTroubleshooting 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:... |
TipRun
docker history your-image:tag --no-truncto 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.
RememberImage 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 MistakeUsing the
:latesttag in production. Thelatesttag is just a convention — it means whatever was last pushed with that tag. If someone pushes a broken build and tags itlatest, every deployment that pullslatestwill get the broken build. Always tag images with an immutable identifier like a git SHA or a semantic version.
SecurityBase images inherit CVEs (known vulnerabilities) from the operating system packages they include. An
ubuntu:22.04base typically has 200+ known CVEs. Analpine:3.18base 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).