Anatomy of a Docker Image
Anatomy of a Docker Image
A Docker image is composed of layers — each layer represents a filesystem change. Layers are stacked and merged using a Union Filesystem (overlay2, aufs) to create the final container filesystem.
Layer Structure
Consider this Dockerfile:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3
COPY requirements.txt /app/
RUN pip3 install -r /app/requirements.txt
COPY . /app
CMD ["python3", "/app/main.py"]
Each instruction produces a layer:
Layer 1: Ubuntu 22.04 base (~78MB)
Layer 2: Python 3 installation (~60MB)
Layer 3: requirements.txt copy (~1KB)
Layer 4: pip install dependencies (~45MB)
Layer 5: Application code (~2MB)
Layer 6: CMD metadata (0 bytes)
How Layers Work
- Layers are read-only and shared between images. If two images use
ubuntu:22.04as base, they share the same base layer on disk. - When a container runs, Docker adds a thin writable layer on top. All changes (new files, modifications, deletions) go here.
- Docker uses content-addressable storage — each layer is identified by its SHA256 hash. If the hash matches an existing layer, Docker uses the cached version.
Layer Inspection
# View image layers with sizes
docker history nginx:1.25
IMAGE CREATED CREATED BY SIZE
a9edb18cadd1 2 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
81d61046be0f 2 weeks ago /bin/sh -c #(nop) STOPSIGNAL SIGQUIT 0B
a2abf6c4d29d 2 weeks ago /bin/sh -c #(nop) EXPOSE map[80/tcp:{}] 0B
d76491233e58 2 weeks ago /bin/sh -c set -x && groupadd --system -… 110MB
Immutable Infrastructure
Images are immutable. Once built, a layer never changes. This immutability provides:
- Reproducibility: The same image produces identical containers every time
- Safety: You cannot accidentally modify a running image
- Efficiency: Layers are shared and cached across hosts
- Auditability: You can trace exactly what changed between versions
Pull, Tag, and Push Operations
Pull, Tag, and Push Operations
Pulling Images
# Pull latest tag
docker pull nginx
# Pull specific version
docker pull nginx:1.25-alpine
# Pull from Docker Hub (explicit)
docker pull docker.io/library/nginx:1.25
# Pull from private registry
docker pull registry.company.com/myapp:v2.1
# Pull by digest (immutable reference)
docker pull nginx@sha256:abc123def456789...
When you pull an image, Docker downloads only layers not already present locally. This makes subsequent pulls very fast.
Tagging Images
Tags are human-readable labels pointing to image digests:
# Tag an existing image
docker tag nginx:1.25 myrepo/nginx:v1
# Tag with registry prefix
docker tag myapp:latest registry.company.com/myapp:latest
# Tag for multiple registries
docker tag myapp:latest gcr.io/myproject/myapp:latest
docker tag myapp:latest 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
Pushing Images
Before pushing, you must authenticate:
# Log in to Docker Hub
docker login
# Log in to private registry
docker login registry.company.com
# Push image
docker push myrepo/nginx:v1
# Push to ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
123456789.dkr.ecr.us-east-1.amazonaws.com
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
Image Naming Conventions
# Docker Hub official image
nginx:1.25
# Docker Hub user image
username/myapp:v2
# Private registry
registry.company.com/team/project:tag
# Cloud registry
gcr.io/project-id/image:tag
123456789.dkr.ecr.us-east-1.amazonaws.com/image:tag
Best Practices for Tagging
- Never use
:latestin production — it creates ambiguity - Use semantic versioning:
myapp:1.2.3 - Include build metadata:
myapp:1.2.3-abc1234(git SHA) - Use immutable digests for critical deployments:
myapp@sha256:...
Inspecting Image Contents
Inspecting Image Contents
Inspect Image Metadata
# Full JSON metadata
docker inspect nginx:1.25
# Get architecture
docker inspect --format '{{.Architecture}}' nginx:1.25
# Get environment variables
docker inspect --format '{{.Config.Env}}' nginx:1.25
# Get exposed ports
docker inspect --format '{{.Config.ExposedPorts}}' nginx:1.25
# Get entrypoint
docker inspect --format '{{.Config.Entrypoint}}' nginx:1.25
# Get working directory
docker inspect --format '{{.Config.WorkingDir}}' nginx:1.25
# Get all layers
docker inspect --format '{{.RootFS.Layers}}' nginx:1.25
View Image History
# Show build history with commands
docker history nginx:1.25
# Show non-truncated
docker history --no-trunc nginx:1.25
# Show only created-by column
docker history --format "{{.CreatedBy}}" nginx:1.25
Diff and Changes
# See filesystem changes in a container
docker diff web
# Output format:
# A /tmp/new-file (Added)
# C /etc/config.conf (Changed)
# D /old-file (Deleted)
Export Image Contents
# Export entire image filesystem as tar
docker save nginx:1.25 | tar -tvf - | head -50
# Extract specific files
docker create --name temp nginx:1.25
docker cp temp:/etc/nginx/nginx.conf ./
docker rm temp
# Or use docker export
docker export temp | tar -xf - --include="etc/nginx/nginx.conf"
Analyze Image Size
# See total image size
docker images nginx:1.25
REPOSITORY TAG IMAGE ID SIZE
nginx 1.25 a9edb18cadd1 187MB
# Compare sizes
docker images --format "{{.Repository}}:{{.Tag}} {{.Size}}" | sort -k2 -h
# Find largest images
docker system df -v
Trivy Security Scan
# Install Trivy
sudo apt-get install trivy
# Scan image for vulnerabilities
trivy image nginx:1.25
# Scan with severity filter
trivy image --severity HIGH,CRITICAL nginx:1.25
# Scan Dockerfile
docker scout cves nginx:1.25
Using .dockerignore Effectively
Using .dockerignore Effectively
A .dockerignore file excludes files and directories from the build context sent to the Docker daemon. Without it, every file in the build directory is sent, including .git, node_modules, and build artifacts.
Why .dockerignore Matters
- Build speed: Sending a 2GB project with
node_modulestakes minutes. With.dockerignore, it takes seconds - Cache efficiency: Changing a file in the build context invalidates the cache for subsequent layers
- Security: Prevents secrets (
.env,.ssh,.aws) from being included in images - Image size: Reduces final image size by excluding unnecessary files
Example .dockerignore
# Version control
.git
.gitignore
.github
# Dependencies
node_modules
vendor
*.jar
# Build artifacts
dist
build
*.o
*.a
# Environment and secrets
.env
.env.*
*.pem
*.key
# IDE and editor
.vscode
.idea
*.swp
*.swo
# Docker files
Dockerfile*
docker-compose*.yml
.dockerignore
# Documentation
README.md
LICENSE
docs/
# OS files
.DS_Store
Thumbs.db
# Test files
tests/
spec/
__tests__/
coverage/
Multi-Language Examples
Python:
__pycache__
*.pyc
*.pyo
.pytest_cache
.venv
venv
*.egg-info
dist
build
.env
Go:
*.exe
test/
vendor/
.git
Java:
target/
*.class
*.jar
.mvn/
.idea/
*.iml
Build Context
# Build with default context (current directory)
docker build -t myapp .
# Build from a specific directory
docker build -t myapp -f Dockerfile /path/to/project
# Build from stdin (no build context)
docker build -t myapp - <<EOF
FROM alpine
copy . /app
EOF
When Docker runs docker build ., it tars the entire directory (minus .dockerignore exclusions) and sends it to the daemon. A large build context means slower builds and higher memory usage.