🐳 Week 4: Containers & Docker

Day 2: Docker Images & Dockerfiles

⏱ Duration: 5 Hours

📚 Learning Objectives

  • Understand Docker images and layers
  • Write effective Dockerfiles
  • Master FROM, COPY, RUN, CMD, ENTRYPOINT instructions
  • Build and tag custom images
  • Understand image caching and optimization

📖 Core Concepts (2 Hours)

What is a Docker Image?

A Docker image is a read-only template containing instructions for creating a container. Images are built in layers, where each instruction adds a new layer.

Image Structure: ┌─────────────────────────────┐ │ Layer 4: CMD (entry point) │ ← Your app command ├─────────────────────────────┤ │ Layer 3: COPY app files │ ← Your application ├─────────────────────────────┤ │ Layer 2: RUN install deps │ ← Dependencies ├─────────────────────────────┤ │ Layer 1: Base OS (Ubuntu) │ ← FROM instruction └─────────────────────────────┘ Each layer is cached - unchanged layers are reused!

Dockerfile Basics

A Dockerfile is a text file with instructions to build an image. Each instruction creates a layer.

# Simple Dockerfile structure FROM base_image:tag # Starting point WORKDIR /app # Set working directory COPY source destination # Copy files RUN command # Execute during build ENV VAR=value # Set environment variable EXPOSE port # Document port usage CMD ["executable", "args"] # Default command at runtime

Essential Dockerfile Instructions

# FROM - Base image (always first) FROM ubuntu:22.04 FROM python:3.11-slim FROM node:18-alpine # WORKDIR - Set working directory WORKDIR /app WORKDIR /home/user/project # COPY - Copy files from host to image COPY . . # Copy all to WORKDIR COPY package.json . # Copy specific file COPY src/ /app/src/ # Copy directory # ADD - Like COPY but can extract archives & fetch URLs ADD app.tar.gz /app/ # Auto-extracts ADD https://example.com/file /app/ # RUN - Execute commands during build RUN apt-get update && apt-get install -y curl RUN pip install -r requirements.txt RUN npm install # ENV - Set environment variables ENV NODE_ENV=production ENV PATH="/app/bin:$PATH" # EXPOSE - Document which ports are used EXPOSE 80 EXPOSE 3000 8080 # CMD - Default command (can be overridden) CMD ["python", "app.py"] CMD ["npm", "start"] # ENTRYPOINT - Fixed command (args appended) ENTRYPOINT ["python"] CMD ["app.py"] # docker run image other.py → python other.py

CMD vs ENTRYPOINT

# CMD - Default command, easily overridden FROM python:3.11 CMD ["python", "app.py"] # docker run image → python app.py # docker run image bash → bash (CMD replaced) # ENTRYPOINT - Fixed executable FROM python:3.11 ENTRYPOINT ["python"] CMD ["app.py"] # docker run image → python app.py # docker run image test.py → python test.py (args to ENTRYPOINT) # Common pattern: ENTRYPOINT for executable, CMD for default args ENTRYPOINT ["./script.sh"] CMD ["--help"]

Building Images

# Build image from Dockerfile docker build -t myapp . docker build -t myapp:v1.0 . docker build -t username/myapp:latest . # Build with different Dockerfile docker build -f Dockerfile.prod -t myapp:prod . # Build with build arguments docker build --build-arg VERSION=1.0 -t myapp . # View build history docker history myapp # List images docker images docker images myapp # Remove images docker rmi myapp docker rmi myapp:v1.0 docker image prune # Remove dangling images

Understanding Layers & Caching

# BAD - Cache invalidated on any code change FROM python:3.11 COPY . . RUN pip install -r requirements.txt CMD ["python", "app.py"] # GOOD - Dependencies cached separately FROM python:3.11 WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "app.py"] # Order matters! Put frequently changing files last

🔬 Hands-on Lab (2.5 Hours)

Lab 1: Build a Python Web App Image

  • Create a simple Flask application
  • Write a Dockerfile
  • Build and run the container
# Create project directory mkdir flask-app && cd flask-app # app.py cat > app.py << 'EOF' from flask import Flask app = Flask(__name__) @app.route('/') def hello(): return '

Hello from Docker!

' if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) EOF # requirements.txt echo "flask==3.0.0" > requirements.txt # 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 CMD ["python", "app.py"] EOF # Build and run docker build -t flask-app . docker run -d -p 5000:5000 --name myflask flask-app curl http://localhost:5000 docker logs myflask docker stop myflask && docker rm myflask

Lab 2: Build a Node.js App Image

  • Create an Express.js application
  • Optimize Dockerfile for caching
  • Use build arguments
# Create project mkdir node-app && cd node-app # package.json cat > package.json << 'EOF' { "name": "docker-node-app", "version": "1.0.0", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "express": "^4.18.2" } } EOF # server.js cat > server.js << 'EOF' const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; app.get('/', (req, res) => { res.json({ message: 'Hello from Dockerized Node.js!', version: process.env.APP_VERSION || 'dev' }); }); app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); EOF # Dockerfile cat > Dockerfile << 'EOF' FROM node:18-alpine ARG APP_VERSION=1.0.0 ENV APP_VERSION=$APP_VERSION WORKDIR /app # Copy package files first (better caching) COPY package*.json ./ RUN npm install --production COPY server.js . EXPOSE 3000 CMD ["npm", "start"] EOF # Build with version docker build --build-arg APP_VERSION=2.0.0 -t node-app:2.0 . docker run -d -p 3000:3000 --name mynode node-app:2.0 curl http://localhost:3000 docker stop mynode && docker rm mynode

Lab 3: Inspect Image Layers

  • View image history
  • Understand layer sizes
  • Compare different base images
# View image layers docker history flask-app docker history node-app:2.0 # Inspect image details docker inspect flask-app docker inspect --format='{{.Config.Env}}' flask-app # Compare base image sizes docker pull python:3.11 docker pull python:3.11-slim docker pull python:3.11-alpine docker images python # See the size difference! # python:3.11 ~1GB # python:3.11-slim ~150MB # python:3.11-alpine ~50MB

✅ Day 2 Checklist

  • Understand Docker images and layers
  • Can write Dockerfiles with FROM, COPY, RUN, CMD
  • Know difference between CMD and ENTRYPOINT
  • Can build and tag images
  • Understand layer caching and optimization