Skip to main content

Container Image

A read-only, layered filesystem snapshot containing an application and everything it needs to run — code, runtime, libraries, and configuration. Container images are the templates from which containers are created.

Container Image — The Template for Every Container

What Is a Container Image in Simple Terms?

A container image is a packaged, portable snapshot of a complete application environment. It contains your application code, the runtime it needs (Node.js, Python, Go), all the library dependencies, configuration files, and even the operating system files needed to run. When you run an image, Docker creates a container from it.

The relationship between an image and a container is exactly like the relationship between a class and an object in programming — the image is the class definition (static, never changes), the container is the running instance (active, has state).

Bash
+------------------------------------------+
| Container Image (read-only template) |
| |
| OS files (Alpine Linux) |
| + Node.js 20 runtime |
| + npm packages (node_modules) |
| + application source code |
| + environment defaults |
+------------------------------------------+
|
| docker run (creates)
|
+--------v---------+ +------------------+
| Container 1 | | Container 2 |
| Running instance | | Running instance |
| Has own RW layer | | Has own RW layer |
| Files it writes | | Files it writes |
| are unique to it | | are unique to it |
+------------------+ +------------------+
Both share the same image layers
No disk duplication

How Images Are Built

Container images are built from a Dockerfile — a text file with instructions that Docker executes one by one, each creating a new layer:

Dockerfile
# Each instruction creates one layer in the image
FROM node:20-alpine # Layer 1: base OS + Node.js (55MB)
WORKDIR /app # Layer 2: set working directory (0B)
COPY package.json ./ # Layer 3: dependency manifest (4KB)
RUN npm ci --omit=dev # Layer 4: installed packages (150MB)
COPY dist/ ./dist/ # Layer 5: compiled source code (2MB)
CMD ["node", "dist/server"] # Layer 6: default startup command (0B)
# Final image: ~207MB, 6 layers
Bash
# Build an image from a Dockerfile
docker build -t payment-api:v3.1.0 .
# See all layers and their sizes
docker history payment-api:v3.1.0
# IMAGE CREATED BY SIZE
# a84f9c2b1d3e CMD ["node" "dist/server"] 0B
# b72c8a9f4e1d COPY dist/ ./dist/ 2MB
# c91d8b3f2a5e RUN npm ci --omit=dev 150MB <- biggest layer
# ... FROM node:20-alpine 55MB

Image Layers and Storage Efficiency

◈ DIAGRAM
Three containers running from the same image:
+----------+ +----------+ +----------+
| api-1 RW | | api-2 RW | | api-3 RW |
| ~1MB | | ~1MB | | ~1MB |
+----------+ +----------+ +----------+
| | |
+----+----+----+----+
|
+---------v----------+
| Shared image layers|
| ~207MB (one copy) |
+--------------------+
Total disk used: 210MB (not 207 x 3 = 621MB)
Each container only adds its unique read-write layer

Image Naming and Tagging

Bash
# Image reference format: [registry/][owner/]name[:tag][@digest]
# Docker Hub (default registry)
nginx # = docker.io/library/nginx:latest
nginx:1.25 # specific version
# Private registry
registry.razorpay.in/payment-api:v3.1.0
905418385260.dkr.ecr.ap-south-1.amazonaws.com/payment-api:v3.1.0
# Tag an image with a new name
docker tag payment-api: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
# List local images
docker images
# REPOSITORY TAG IMAGE ID SIZE
# payment-api v3.1.0 a84f9c2b1d3e 207MB
# nginx 1.25 b72c8a9f4e1d 187MB
# Remove an image
docker rmi payment-api:v3.1.0
# Remove all unused images
docker image prune -a

Inspecting Images

Bash
# Full image metadata
docker image inspect nginx:1.25
# Returns JSON with: layers, exposed ports, environment, entrypoint, labels
# See image layers and sizes
docker history nginx:1.25 --no-trunc
# Check image size
docker images nginx:1.25
# REPOSITORY TAG SIZE
# nginx 1.25 187MB
# Scan for vulnerabilities
trivy image nginx:1.25
# Finds CVEs in OS packages and application dependencies
# Analyse layer efficiency
dive nginx:1.25
# Interactive TUI showing each layer content

Image vs Container — Key Differences

Image Container
State Static, read-only Running, has read-write layer
Storage On disk as layers In memory + thin RW layer
Lifetime Permanent until deleted Created and destroyed
Count One image Many containers from one image
Modification Requires rebuilding Writes go to container RW layer

Troubleshooting Reference

Problem Cause Fix
pull access denied Not logged in to private registry docker login registry-hostname
image not found Wrong tag or registry Check exact image name and tag
Large image size Dev dependencies, wrong base image Multi-stage build, use alpine base
Image has CRITICAL CVEs Outdated base image or packages Update base image, run trivy image
Disk full on build machine Old images accumulating docker image prune -a -f
Tip

Use docker history image:tag to understand what is inside an image before running it. Each line shows one Dockerfile instruction and how much space it added. The largest layers are your optimisation targets — usually RUN npm install or RUN apt-get install instructions.

Remember

Image layers are immutable and shared. If you have 10 containers all running from nginx:1.25, the 187MB image is stored once on disk — not 10 times. Each container only adds a tiny read-write layer for its own changes. This efficiency is a core reason containers are so much faster and cheaper than VMs.

Common Mistake

Using :latest as the image tag in production. The latest tag is mutable — it points to whatever was last pushed with that tag. If a broken build is tagged latest, every deployment gets the broken version. Always use an immutable tag like a git commit SHA or semantic version: payment-api:a84f9c2b or payment-api:v3.1.0.

Frequently Asked Questions

Why are container images built in layers instead of as a single flat filesystem?

Each instruction in a Dockerfile (RUN, COPY, ADD) creates a new layer, and layers are content-addressed and cached — so if only your application code changes but base OS and dependency layers stay the same, rebuilding and pulling only transfers the changed layer, not the whole image. This is also why Dockerfile instruction order matters for build speed: put things that change rarely (base image, dependency installs) before things that change often (application code copy).

What's a common mistake that bloats container images?

Not cleaning up build-time artifacts (compiler toolchains, package manager caches, temp files) within the same layer they were created, since a later `rm` in a separate RUN instruction doesn't shrink earlier layers — the deleted files' bytes are still in the image history. Multi-stage builds solve this properly: build in one stage with the full toolchain, then copy only the compiled output into a minimal final-stage image, keeping production images small and reducing attack surface.