Day 9: Layering and Caching in Docker Images
Learning Objectives: By the end of this lesson, you will be able to:
- Understand how Docker image layers work and why they matter
- Optimize Dockerfiles to maximize build cache efficiency
- Analyze image layers using Docker tools and commands
- Apply advanced caching strategies for faster builds and smaller images
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Have you ever noticed that sometimes Docker builds are lightning-fast, while other times they seem to take forever? The secret lies in understanding Docker’s layering and caching system. Today, we’ll unlock this mystery and learn how to make your builds consistently fast and efficient.
Lesson Overview
- Duration: 50-60 minutes
- Prerequisites: Completion of Day 8 (Dockerfile Basics), understanding of basic Docker commands
- Materials Needed: Docker installed, text editor, sample applications from previous lessons
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- Docker Layer Architecture - How layers are created and stored
- Build Cache Mechanics - How Docker decides what to rebuild
- Cache Optimization Strategies - Writing cache-friendly Dockerfiles
- Layer Analysis Tools - Inspecting and understanding your images
- Advanced Techniques - Multi-stage builds and cache mounts
Success Criteria:
By the end of this session, you should be able to write Dockerfiles that build 5-10x faster through effective caching and produce smaller, more efficient images.
3. Foundation Knowledge (15-20 minutes)
Understanding Docker Layers
Every instruction in a Dockerfile creates a new layer in the resulting image. These layers are like transparent sheets stacked on top of each other - each adding something new while preserving everything below.
Real-World Analogy: Think of Docker layers like layers in a cake. Each layer (sponge, cream, frosting) adds to the final product. If you need to change the frosting, you don’t remake the entire cake - you just replace the top layer. Similarly, Docker only rebuilds layers that have changed.
Key Layer Concepts:
- Immutable: Once created, layers never change
- Shareable: Multiple images can share the same layers
- Cacheable: Unchanged layers are reused from cache
- Hierarchical: Each layer builds upon the previous ones
How Docker Build Cache Works
Docker uses a sophisticated caching mechanism to speed up builds:
-
Cache Key Calculation: Docker creates a unique identifier for each layer based on:
- The instruction itself (RUN, COPY, etc.)
- The contents of files being copied
- The previous layer’s cache key
-
Cache Lookup: Before executing an instruction, Docker checks if a layer with the same cache key already exists
-
Cache Hit/Miss:
- Hit: Reuse existing layer (fast)
- Miss: Execute instruction and create new layer (slow)
-
Cache Invalidation: Once a layer cache misses, all subsequent layers must be rebuilt
Key Terminology
- Layer: A read-only filesystem created by a Dockerfile instruction
- Cache Hit: When Docker finds and reuses an existing layer
- Cache Miss: When Docker must execute an instruction and create a new layer
- Cache Invalidation: When changes force rebuilding of layers
- Build Context: The directory sent to Docker daemon containing files for the build
4. Detailed Explanation and Examples (25-30 minutes)
Layer Creation Rules
Instructions That Create Layers
FROM ubuntu:20.04 # Creates base layer
RUN apt-get update # Creates new layer
COPY app.js /app/ # Creates new layer
RUN npm install # Creates new layer
Instructions That Don’t Create Layers
WORKDIR /app # Metadata only
EXPOSE 3000 # Metadata only
ENV NODE_ENV=production # Metadata only
LABEL version="1.0" # Metadata only
Cache Optimization Strategies
1. Order Instructions by Change Frequency
# ❌ Poor caching - source code changes invalidate dependency installation
FROM node:18-alpine
WORKDIR /app
COPY . . # Changes frequently
RUN npm install # Gets invalidated often
# ✅ Good caching - dependencies cached separately from source code
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./ # Changes infrequently
RUN npm install # Cached until package.json changes
COPY . . # Source code changes don't affect dependencies
2. Combine Related Commands
# ❌ Multiple layers for related operations
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN apt-get clean
# ✅ Single layer for related operations
RUN apt-get update && \
apt-get install -y curl wget && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
3. Use Specific File Patterns
# ❌ Copies everything, invalidates cache on any file change
COPY . .
# ✅ Copy only what's needed, when it's needed
COPY package*.json ./
RUN npm install
COPY src/ ./src/
COPY public/ ./public/
Advanced Caching Techniques
Multi-Stage Build Optimization
# Build stage
FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY --from=dependencies /app/node_modules ./node_modules
Cache Mount (BuildKit Feature)
# Use cache mount for package managers
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production
Common Misconceptions
-
Misconception: “More layers always mean larger images” Reality: Layer count doesn’t directly correlate with image size; content and compression matter more
-
Misconception: “Cache is always beneficial” Reality: Sometimes clearing cache is necessary for security updates or debugging
-
Misconception: “All instructions create layers” Reality: Only certain instructions (RUN, COPY, ADD) create filesystem layers
5. Live Coding Demonstration (20-25 minutes)
Example 1: Cache-Inefficient Dockerfile
# This Dockerfile doesn't leverage caching well
FROM node:18-alpine
WORKDIR /app
# This copies everything first - any file change invalidates cache
COPY . .
# Dependencies are reinstalled on every source code change
RUN npm install
# Build happens every time
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Build Performance:
# First build: ~2 minutes
docker build -t inefficient-app .
# Change one line in source code and rebuild: ~2 minutes again!
Example 2: Cache-Optimized Dockerfile
# This Dockerfile maximizes caching efficiency
FROM node:18-alpine AS dependencies
WORKDIR /app
# Copy dependency files first - these change rarely
COPY package*.json ./
# Install dependencies - cached until package.json changes
RUN npm ci --only=production
# Separate stage for development dependencies
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Final stage with only production assets
FROM node:18-alpine
WORKDIR /app
# Copy built application
COPY --from=builder /app/dist ./dist
# Copy production dependencies
COPY --from=dependencies /app/node_modules ./node_modules
# Copy only necessary runtime files
COPY package.json ./
EXPOSE 3000
CMD ["npm", "start"]
Build Performance:
# First build: ~2 minutes
docker build -t optimized-app .
# Change source code and rebuild: ~30 seconds!
# Only the builder stage and final copy are rebuilt
Example 3: Analyzing Layer Impact
# Let's build an image and analyze its layers
FROM ubuntu:20.04
# Layer 1: System updates
RUN apt-get update && apt-get install -y \
curl \
wget \
git \
&& rm -rf /var/lib/apt/lists/*
# Layer 2: Application directory
WORKDIR /app
# Layer 3: Copy application
COPY app.py requirements.txt ./
# Layer 4: Install Python dependencies
RUN pip install -r requirements.txt
# Layer 5: Copy source code
COPY src/ ./src/
CMD ["python", "app.py"]
Analysis Commands:
# Build the image
docker build -t layer-demo .
# Inspect layers
docker history layer-demo
# See detailed layer information
docker image inspect layer-demo
Debugging Cache Issues
# Build without cache to force rebuild
docker build --no-cache -t myapp .
# Build with cache debugging
docker build --progress=plain -t myapp .
# Check what files changed
docker diff <container-id>
6. Hands-On Practice (30-40 minutes)
Exercise 1: Cache Analysis (10 minutes)
Objective: Understand cache behavior by observing build times
Instructions:
- Create the inefficient Dockerfile from the demo
- Build it and time the process
- Make a small change to source code
- Rebuild and observe the time difference
- Now optimize the Dockerfile and repeat
Starter Code:
Create app.js:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.json({
message: 'Hello from optimized Docker!',
version: '1.0.0'
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Create package.json:
{
"name": "cache-demo",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.0",
"lodash": "^4.17.21"
},
"scripts": {
"start": "node app.js"
}
}
Task: Write two Dockerfiles - one cache-inefficient and one optimized.
Exercise 2: Layer Inspection (15 minutes)
Objective: Learn to analyze and understand image layers
Instructions:
- Build an image with your optimized Dockerfile
- Use
docker historyto examine layers - Calculate the size impact of each layer
- Identify opportunities for further optimization
Commands to use:
# Examine layer history
docker history <image-name>
# Get detailed layer information
docker image inspect <image-name>
# Compare image sizes
docker images
Analysis Questions:
- Which layers are largest?
- Which layers change most frequently?
- How can you reduce the number of layers?
Exercise 3: Multi-Stage Build Optimization (15 minutes)
Objective: Create an efficient multi-stage build for a Python application
Challenge: Build a Python Flask application using multi-stage builds to:
- Install build dependencies in one stage
- Build/compile the application in another stage
- Create a minimal runtime image
Starter Files:
Create app.py:
from flask import Flask, jsonify
import requests
app = Flask(__name__)
@app.route('/')
def hello():
return jsonify({
'message': 'Hello from Flask!',
'status': 'optimized'
})
@app.route('/health')
def health():
return jsonify({'status': 'healthy'})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Create requirements.txt:
Flask==2.3.0
requests==2.31.0
gunicorn==21.2.0
Hints:
- Use python:3.9 for building and python:3.9-slim for runtime
- Install build dependencies separately from runtime dependencies
- Use gunicorn for production serving
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
- Problem: Cache never hits despite no changes Cause: Including timestamp or random data in build context Solution: Use .dockerignore to exclude changing files
# Exclude files that change frequently but aren't needed
*.log
.git
.DS_Store
node_modules/.cache
- Problem: Dependencies reinstall on every source code change Cause: Copying source code before installing dependencies Solution: Reorder Dockerfile instructions
# ❌ Wrong order
COPY . .
RUN npm install
# ✅ Correct order
COPY package*.json ./
RUN npm install
COPY . .
- Problem: Large layers due to inefficient cleanup Cause: Cleaning up in separate RUN instructions Solution: Clean up in the same layer where mess is created
# ❌ Creates large layer
RUN apt-get update && apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# ✅ Efficient cleanup
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
Debugging Tips
- Use
docker build --progress=plainto see detailed build output - Use
docker system dfto see Docker disk usage - Use
docker image pruneto clean up unused layers - Monitor build times and look for sudden increases
Cache Troubleshooting Commands
# Clear all build cache
docker builder prune
# Clear only unused cache
docker builder prune --filter "until=24h"
# Build without cache
docker build --no-cache -t myapp .
# Check cache usage
docker system df
8. Best Practices and Professional Tips (10 minutes)
Industry Standards
Dockerfile Layer Optimization
# ✅ Optimized production Dockerfile
FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && \
npm cache clean --force
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && \
npm prune --production
FROM node:18-alpine
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
WORKDIR /app
COPY --from=dependencies --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --chown=nextjs:nodejs package.json ./
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
BuildKit Features for Advanced Caching
# syntax=docker/dockerfile:1.4
FROM node:18-alpine
WORKDIR /app
# Use cache mounts for package managers
RUN --mount=type=cache,target=/root/.npm \
--mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
npm ci --only=production
Performance Monitoring
- Track build times in CI/CD pipelines
- Monitor image sizes over time
- Set up alerts for unexpected size increases
- Use tools like
diveto analyze layer efficiency
Next Steps in Your Learning Journey
- Learn about Docker BuildKit features
- Explore advanced caching with registry caching
- Study container image scanning for security
- Practice with different language ecosystems
9. Summary and Review (5 minutes)
Key Takeaways
- Layer order matters significantly - Put frequently changing instructions last
- Cache invalidation is cascading - Once a layer changes, all subsequent layers rebuild
- Multi-stage builds optimize both cache and size - Use them for complex applications
Quick Review Questions
- What happens to cache when you modify package.json in a Node.js application?
- Why should you combine related RUN instructions?
- How does multi-stage building help with caching?
Answers:
- All layers after the package.json COPY instruction will be invalidated and rebuilt
- To reduce layer count and enable cleanup in the same layer, reducing final image size
- It allows caching of dependencies separately from source code and creates smaller final images
For Instructors
Teaching Tips
- Use timing examples to make cache benefits tangible
- Show before/after build times with real numbers
- Use visual diagrams to explain layer concepts
- Demonstrate cache invalidation with live examples
Assessment Ideas
- Build time comparison exercise
- Layer analysis assignments
- Dockerfile optimization challenges
- Real-world application optimization projects
Adaptation Notes
- For beginners: Focus on basic layer concepts and simple optimizations
- For advanced students: Introduce BuildKit features and advanced caching strategies
- Online delivery: Use screen sharing for timing demonstrations
- Time management: Core concepts need 25-30 minutes, exercises can be extended or shortened