Day 6: Working with Docker Images - pull, build, tag

Learning Objectives: By the end of this lesson, you will be able to:

  • Pull images from registries with different strategies and options
  • Build custom images using Dockerfiles with best practices
  • Tag images effectively for version control and distribution
  • Manage local image storage and understand image layers
  • Push images to registries and share your custom containers

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Think about how you create a custom photo album: you might start with existing photos (pull), combine and edit them to create something new (build), label them with meaningful names (tag), and then share your album with friends (push). Docker image management works very similarly!

Lesson Overview

  • Duration: 60-75 minutes
  • Prerequisites: Docker basics, understanding of containers and Docker Hub
  • Materials Needed: Docker installed, text editor, sample application code

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Image Pulling Strategies - Efficient downloading and caching
  2. Dockerfile Fundamentals - Writing instructions to build custom images
  3. Building Images - Creating containers from source code
  4. Tagging Systems - Organizing and versioning your images
  5. Image Management - Storage, cleanup, and optimization
  6. Sharing Images - Pushing to registries for distribution

Success Criteria:

By the end of this session, you should be able to create, manage, and distribute custom Docker images following industry best practices.

3. Foundation Knowledge (15-20 minutes)

Understanding Docker Images

Docker images are read-only templates used to create containers. They consist of multiple layers, each representing a set of file changes. Understanding this layered architecture is crucial for efficient image management.

Real-World Analogy: Think of Docker images like a cake recipe and its layers. Each layer adds ingredients (files, configurations, software), and the final image is the complete recipe that can be used to bake identical cakes (containers).

Why This Matters: Efficient image management reduces storage costs, improves deployment speed, and enables better collaboration in development teams.

The Image Lifecycle

  1. Pull - Download base images or dependencies
  2. Build - Create custom images from Dockerfiles
  3. Tag - Label images for organization and versioning
  4. Push - Upload images to registries for sharing
  5. Deploy - Run containers from images

Layer Architecture Benefits

  • Caching - Unchanged layers are reused, speeding up builds
  • Storage Efficiency - Common layers are shared between images
  • Network Optimization - Only changed layers need to be transferred
  • Version Control - Each layer represents a specific change

Key Terminology

  • Base Image: Starting point for building custom images
  • Layer: Individual instruction result in a Dockerfile
  • Image ID: Unique identifier for each image
  • Tag: Human-readable label for image versions
  • Registry: Storage and distribution system for images
  • Repository: Collection of related images with different tags

4. Detailed Explanation and Examples (25-30 minutes)

Image Pulling Strategies

Pulling images efficiently involves understanding tags, platform considerations, and caching mechanisms.

Pull Command Variations

  • Latest Version: docker pull nginx (defaults to latest tag)
  • Specific Version: docker pull nginx:1.23.1
  • Platform Specific: docker pull --platform linux/amd64 nginx
  • All Tags: docker pull -a nginx (downloads all available tags)

Strategic Pulling Considerations

  • Base Image Selection: Choose appropriate base images for your needs
  • Version Pinning: Use specific versions for reproducible builds
  • Platform Compatibility: Ensure images match your deployment architecture
  • Cache Utilization: Leverage Docker’s layer caching for faster subsequent pulls

Dockerfile Fundamentals

Dockerfiles are text files containing instructions to build images. Each instruction creates a new layer in the image.

Essential Instructions

  • FROM: Specifies the base image
  • RUN: Executes commands during build
  • COPY/ADD: Adds files from build context
  • WORKDIR: Sets working directory
  • EXPOSE: Documents which ports the container uses
  • CMD/ENTRYPOINT: Defines container startup behavior

Best Practices for Dockerfiles

  • Start with minimal base images
  • Combine multiple RUN commands to reduce layers
  • Order instructions from least to most likely to change
  • Use .dockerignore to exclude unnecessary files
  • Set appropriate user permissions

Tagging Strategy and Version Management

Effective tagging enables organized image management and deployment strategies.

Common Tagging Patterns

  • Semantic Versioning: myapp:1.2.3, myapp:1.2, myapp:1
  • Environment Tags: myapp:dev, myapp:staging, myapp:prod
  • Build Information: myapp:commit-abc123, myapp:build-456
  • Date-based: myapp:2025-06-27, myapp:20250627

Professional Tagging Strategies

  • Always tag images with specific versions
  • Use latest tag sparingly and only for stable releases
  • Implement consistent naming conventions across projects
  • Include metadata in tags when helpful

5. Live Coding Demonstration (20-25 minutes)

Example 1: Basic Image Operations

# Pull different versions of an image
docker pull alpine:3.18
docker pull alpine:3.17
docker pull alpine:latest

# List local images
docker images alpine

# Inspect image details
docker inspect alpine:3.18

# Check image layers
docker history alpine:3.18

Explanation:

  • Different Alpine versions show how tags work
  • docker images displays local image inventory
  • docker inspect reveals detailed image metadata
  • docker history shows the layer composition

Example 2: Building a Simple Custom Image

First, let’s create a simple web application:

# Create project directory
mkdir my-web-app
cd my-web-app

# Create a simple HTML file
cat > index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
    <title>My Docker App</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
        .container { max-width: 600px; margin: 0 auto; padding: 20px; }
        .highlight { color: #0066cc; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Welcome to My Docker App!</h1>
        <p>This is a <span class="highlight">custom image</span> built with Docker.</p>
        <p>Build time: <span id="datetime"></span></p>
    </div>
    <script>
        document.getElementById('datetime').textContent = new Date().toLocaleString();
    </script>
</body>
</html>
EOF

Now create a Dockerfile:

# Dockerfile
FROM nginx:alpine

# Copy our custom HTML file
COPY index.html /usr/share/nginx/html/index.html

# Expose port 80
EXPOSE 80

# The base nginx image already has a CMD, so we don't need to specify one

Build and test the image:

# Build the image with a tag
docker build -t my-web-app:1.0 .

# Run a container from our custom image
docker run -d -p 8080:80 --name my-app my-web-app:1.0

# Test the application
curl http://localhost:8080

# Clean up
docker stop my-app
docker rm my-app

Key Points:

  • FROM specifies the base image (nginx:alpine)
  • COPY adds our custom HTML file
  • -t flag tags the image during build
  • The build context (.) contains files needed for the build

Example 3: Advanced Dockerfile with Multiple Stages

# Multi-stage Dockerfile example
FROM node:18-alpine AS builder

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Build the application
RUN npm run build

# Production stage
FROM nginx:alpine AS production

# Copy built application from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html

# Add custom nginx configuration if needed
# COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Example 4: Comprehensive Tagging Examples

# Build with multiple tags
docker build -t my-web-app:1.0 -t my-web-app:latest .

# Tag an existing image with new tags
docker tag my-web-app:1.0 my-web-app:stable
docker tag my-web-app:1.0 my-web-app:prod-ready

# Create environment-specific tags
docker tag my-web-app:1.0 my-web-app:dev
docker tag my-web-app:1.0 my-web-app:staging

# List all tags for the image
docker images my-web-app

# Remove specific tags
docker rmi my-web-app:dev
docker rmi my-web-app:staging

Debugging Demonstration

# Common build errors and solutions

# Error: Build context too large
# Solution: Use .dockerignore file
cat > .dockerignore << 'EOF'
node_modules
.git
*.log
.env
EOF

# Error: COPY failed - file not found
# Solution: Ensure files exist in build context
ls -la  # Verify files are present

# Error: Layer caching issues
# Solution: Use --no-cache flag
docker build --no-cache -t my-web-app:1.1 .

# Debug: Inspect failed builds
docker build -t debug-build . || docker run --rm -it $(docker images -q | head -1) sh

6. Hands-On Practice (30-40 minutes)

Exercise 1: Custom Python Application Image (15 minutes)

Objective: Build a custom image for a Python web application

Instructions:

  1. Create a simple Python Flask application
  2. Write a Dockerfile for the application
  3. Build the image with appropriate tags
  4. Test the containerized application

Starter Code:

# app.py
from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return f"""
    <h1>Hello from Docker!</h1>
    <p>Environment: {os.environ.get('ENV', 'development')}</p>
    <p>Version: {os.environ.get('VERSION', '1.0.0')}</p>
    """

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Solution:

# Create project structure
mkdir python-docker-app
cd python-docker-app

# Create app.py (use code above)
cat > app.py << 'EOF'
from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return f"""
    <h1>Hello from Docker!</h1>
    <p>Environment: {os.environ.get('ENV', 'development')}</p>
    <p>Version: {os.environ.get('VERSION', '1.0.0')}</p>
    """

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
EOF

# Create requirements.txt
echo "Flask==2.3.2" > requirements.txt

# Create Dockerfile
cat > Dockerfile << 'EOF'
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 5000

ENV ENV=production
ENV VERSION=1.0.0

CMD ["python", "app.py"]
EOF

# Build with multiple tags
docker build -t python-app:1.0.0 -t python-app:latest .

# Test the application
docker run -d -p 5000:5000 --name python-test python-app:1.0.0

# Verify it works
curl http://localhost:5000

# Clean up
docker stop python-test
docker rm python-test

Exercise 2: Image Optimization Challenge (15 minutes)

Objective: Optimize a Docker image for size and build speed

Challenge: Take the Python application from Exercise 1 and optimize it using:

  • Multi-stage builds
  • Alpine base images
  • Efficient layer ordering
  • Build cache optimization

Requirements:

  • Reduce final image size by at least 50%
  • Implement build caching for dependencies
  • Add health check functionality

Solution:

# Optimized Dockerfile
FROM python:3.11-alpine AS base

# Install system dependencies in one layer
RUN apk add --no-cache gcc musl-dev

FROM base AS dependencies

WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM python:3.11-alpine AS production

# Copy only the installed packages
COPY --from=dependencies /root/.local /root/.local

WORKDIR /app
COPY app.py .

# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:5000/ || exit 1

EXPOSE 5000

ENV ENV=production
ENV VERSION=1.0.0
ENV PATH=/root/.local/bin:$PATH

CMD ["python", "app.py"]
# Build optimized version
docker build -f Dockerfile.optimized -t python-app:1.0.0-optimized .

# Compare image sizes
docker images python-app

# Test optimized image
docker run -d -p 5001:5000 --name python-optimized python-app:1.0.0-optimized

# Verify health check
docker ps  # Should show health status

# Clean up
docker stop python-optimized
docker rm python-optimized

Exercise 3: Image Registry Simulation (10 minutes)

Objective: Practice tagging for different environments and sharing strategies

Challenge: Create a tagging strategy for a microservice that will be deployed across multiple environments.

Requirements:

  • Create tags for dev, staging, and production environments
  • Implement version-based tagging
  • Simulate pushing to different registry namespaces

Solution:

# Build base image
docker build -t microservice:base .

# Create environment-specific tags
docker tag microservice:base microservice:dev-latest
docker tag microservice:base microservice:staging-1.0.0
docker tag microservice:base microservice:prod-1.0.0

# Create registry-namespace tags (simulate different registries)
docker tag microservice:base localhost:5000/company/microservice:1.0.0
docker tag microservice:base registry.company.com/team/microservice:1.0.0

# Create feature branch tag
docker tag microservice:base microservice:feature-user-auth

# List all tags
docker images microservice

# Simulate registry operations (would normally push to actual registry)
echo "Would push to registries:"
echo "docker push localhost:5000/company/microservice:1.0.0"
echo "docker push registry.company.com/team/microservice:1.0.0"

# Clean up tags
docker rmi microservice:dev-latest
docker rmi microservice:staging-1.0.0
docker rmi microservice:feature-user-auth

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Large image sizes due to inefficient Dockerfiles Cause: Including unnecessary files, not using multi-stage builds, or inefficient layer ordering Solution: Use .dockerignore, multi-stage builds, and combine RUN commands

  2. Problem: Build cache not working effectively Cause: Copying files before installing dependencies or poor instruction ordering Solution: Order Dockerfile instructions from least to most likely to change

  3. Problem: Tag confusion and image proliferation Cause: Inconsistent tagging strategy and not cleaning up old images Solution: Implement consistent tagging conventions and regular cleanup

Debugging Tips

  • Use docker build --progress=plain for detailed build output
  • Inspect intermediate layers with docker run --rm -it <layer-id> sh
  • Use docker system df to monitor disk usage
  • Leverage BuildKit for advanced build features and better caching
  • Use docker scout (if available) for vulnerability scanning

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Dockerfile Organization: Keep Dockerfiles in version control with your application code
  • Image Naming: Use consistent naming conventions: registry/namespace/repository:tag
  • Security: Run containers as non-root users when possible
  • Size Optimization: Use multi-stage builds and minimal base images

Production Guidelines

  • Pin base image versions for reproducibility
  • Implement automated image scanning in CI/CD pipelines
  • Use semantic versioning for application images
  • Maintain image documentation and changelogs
  • Implement image retention policies to manage storage costs

Next Steps in Your Learning Journey

  • Tomorrow: Create your first complete containerized application
  • Practice: Build images for different types of applications (web, API, database)
  • Advanced: Explore BuildKit features, image signing, and security scanning

9. Summary and Review (5 minutes)

Key Takeaways

  1. Image Management Lifecycle: Pull, build, tag, and push form the core of image management
  2. Dockerfile Best Practices: Efficient layer ordering and multi-stage builds optimize images
  3. Tagging Strategy: Consistent tagging enables organized deployment and version control

Quick Review Questions

  1. Why should you order Dockerfile instructions from least to most likely to change?
  2. What’s the difference between COPY and ADD in Dockerfiles?
  3. How do multi-stage builds help optimize images?

Answers:

  1. To maximize Docker’s layer caching, reducing build times when only application code changes
  2. COPY simply copies files; ADD has additional features like URL downloads and archive extraction
  3. Multi-stage builds allow you to use different base images for building and runtime, keeping only necessary files in the final image

For Instructors

Teaching Tips

  • Demonstrate the visual difference in image sizes before and after optimization
  • Show real build times to emphasize the importance of caching
  • Use docker history to illustrate layer composition and size impact
  • Encourage students to experiment with different base images

Assessment Ideas

  • Image optimization challenge: Students optimize a provided inefficient Dockerfile
  • Tagging strategy presentation: Students design and present a tagging strategy for a real-world scenario
  • Build troubleshooting: Provide broken Dockerfiles for students to fix

Adaptation Notes

  • For beginners: Focus on basic build and tag operations, save multi-stage builds for later
  • For advanced students: Include BuildKit features, image signing, and advanced optimization techniques
  • Online delivery: Use screen sharing to show build progress and terminal output
  • Time management: Exercises can be extended or made homework based on available time