Day 41: Dockerfile Creation - Building Custom Docker Images ποΈ

Welcome back! π Day 41 of the 100 Days Cloud DevOps Challenge, and today we're mastering Dockerfile creation! This is how you build production-ready container images - the foundation of containerized application deployment. Let's build! π―
π― The Mission - Create Custom Apache Image
It's application deployment day - building a custom image for the development team:
π TASK TICKET #DEV-8041 - Custom Docker Image Creation
Priority: HIGH
Type: Dockerfile Development & Image Build
Purpose: Custom Apache Image for Application Team
ββββββββββββββββββββββββββββββββββββββββββββββββ
PROJECT: Nautilus Application Development
Team: Nautilus Application Development & DevOps
Location: Stratos Datacenter
Server: App Server 3
ββββββββββββββββββββββββββββββββββββββββββββββββ
BACKGROUND:
ββ Application team needs custom images
ββ Initial testing requirements shared
ββ DevOps team creating Dockerfile
ββ Build process on App Server 3
REQUIREMENTS:
1. Dockerfile Location:
ββ Path: /opt/docker/Dockerfile
ββ Server: App Server 3 (stapp03)
ββ Note: Keep 'D' capital in Dockerfile
2. Base Image Configuration:
ββ Base: ubuntu:24.04
ββ Latest Ubuntu LTS release
ββ Stable and well-supported
3. Apache Installation:
ββ Package: apache2
ββ Configure for port 5004
ββ Do NOT update other settings
ββ Keep default document root
4. Port Configuration:
ββ Apache listen port: 5004
ββ Custom port (non-standard)
ββ No other modifications needed
5. Image Build:
ββ Build from Dockerfile
ββ Tag: None specified (default)
ββ Verify successful build
ββββββββββββββββββββββββββββββββββββββββββββββββ
STATUS: READY TO START
DEADLINE: Today
CRITICAL: Exact specifications required
ββββββββββββββββββββββββββββββββββββββββββββββββ
This is infrastructure as code! Building reproducible, documented container images! π¦
π€ Why Dockerfiles Matter - Reproducible Image Creation
The Manual Container Problem
Without Dockerfile:
Manual process (what we did Day 39-40):
1. docker run ubuntu
2. apt-get update
3. apt-get install apache2
4. Configure files manually
5. docker commit container image
Problems:
β Not reproducible
β No documentation
β Manual steps error-prone
β Can't version control
β Hard to share with team
β Inconsistent builds
With Dockerfile:
Automated Dockerfile:
1. Write Dockerfile once
2. docker build -t image .
3. Same result every time
Benefits:
β
Fully reproducible
β
Self-documenting
β
Version controlled
β
Automated builds
β
CI/CD integration
β
Team collaboration
Real stat: 94% of production container images are built from Dockerfiles! π
Understanding Dockerfiles
What is a Dockerfile?
A Dockerfile is a text file containing instructions to build a Docker image:
Dockerfile Structure:
FROM ubuntu:24.04 β Base image
RUN apt-get update β Execute commands
RUN apt-get install -y β Install packages
COPY file /path β Add files
EXPOSE 5004 β Document ports
CMD ["apache2ctl", "-D", "FOREGROUND"] β Default command
Each instruction creates a layer:
ββ Layer 1: Base Ubuntu
ββ Layer 2: apt-get update
ββ Layer 3: apache2 installed
ββ Layer 4: Files copied
ββ Final image ready!
Dockerfile vs docker commit:
docker commit (Day 39):
ββ Interactive changes
ββ Manual configuration
ββ One-time snapshot
ββ Not reproducible
ββ Good for: Prototyping
Dockerfile (Today):
ββ Scripted instructions
ββ Automated build
ββ Reproducible every time
ββ Version controlled
ββ Good for: Production
Best practice:
ββ Prototype with commit
ββ Productionize with Dockerfile
Dockerfile Instructions
Common Dockerfile instructions:
FROM - Base image (required, must be first)
FROM ubuntu:24.04
RUN - Execute commands during build
RUN apt-get update
RUN apt-get install -y apache2
COPY - Copy files from host to image
COPY app.py /app/
ADD - Like COPY but can extract archives, download URLs
ADD archive.tar.gz /app/
WORKDIR - Set working directory
WORKDIR /app
ENV - Set environment variables
ENV DEBUG=true
EXPOSE - Document which ports are used (metadata only)
EXPOSE 5004
CMD - Default command when container starts
CMD ["apache2ctl", "-D", "FOREGROUND"]
ENTRYPOINT - Configure container as executable
ENTRYPOINT ["python"]
USER - Switch user for subsequent instructions
USER www-data
VOLUME - Create mount point
VOLUME /data
LABEL - Add metadata
LABEL version="1.0"
Real-World Dockerfile Patterns
Pattern 1: Simple Web Server
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Pattern 2: Python Application
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
Pattern 3: Multi-Stage Build
# Build stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# Runtime stage
FROM alpine:latest
COPY --from=builder /app/myapp /
CMD ["/myapp"]
ποΈ Understanding the Setup
App Server 3 Details:
Server: stapp03
User: banner
Password: BigGr33n
Role: Application Server 3
Current State:
ββ Docker installed
ββ /opt/docker/ directory (may need creation)
ββ Ready to create Dockerfile
Target State:
ββ /opt/docker/Dockerfile created
ββ Base: ubuntu:24.04
ββ Apache2 installed
ββ Port 5004 configured
ββ Image built successfully
Dockerfile Architecture:
Our Dockerfile will create:
Base Layer: ubuntu:24.04
ββ Ubuntu 24.04 LTS base
ββ ~77 MB
Layer 2: Package update
ββ apt-get update
ββ Package lists refreshed
Layer 3: Apache installation
ββ apt-get install apache2
ββ ~25 MB added
Layer 4: Port configuration
ββ Modify ports.conf
ββ Modify 000-default.conf
ββ Listen on 5004
Final Image:
ββ Total: ~102 MB
ββ Apache2 on port 5004
ββ Ready to run containers
Build Process:
Step 1: Create /opt/docker/Dockerfile
Step 2: Write Dockerfile instructions
Step 3: docker build -t imagename .
Step 4: Docker executes each instruction
Step 5: Image created and tagged
Step 6: Ready to use
π οΈ Complete Step-by-Step Implementation
Phase 1: Access and Prepare Environment
Step 1.1: SSH to App Server 3
# Connect to App Server 3
ssh banner@stapp03
# Password: BigGr33n
You're logged in! β
Step 1.2: Verify Docker Installation
# Check Docker version
docker --version
Expected output:
Docker version 24.0.7, build afdd53b
Docker installed! β
Step 1.3: Verify Docker Service
# Check Docker daemon status
sudo systemctl status docker
Expected output:
β docker.service - Docker Application Container Engine
Active: active (running)
Docker running! β
Step 1.4: Create Dockerfile Directory
# Create /opt/docker directory if doesn't exist
sudo mkdir -p /opt/docker
Directory ready! β
Step 1.5: Verify Directory Permissions
# Check directory exists
ls -ld /opt/docker
Expected output:
drwxr-xr-x 2 root root 4096 Dec 12 10:00 /opt/docker
Directory created! β
Phase 2: Create Dockerfile
Step 2.1: Navigate to Directory
# Change to /opt/docker
cd /opt/docker
In correct directory! β
Step 2.2: Create Dockerfile
# Create Dockerfile with proper content
sudo tee /opt/docker/Dockerfile > /dev/null << 'EOF'
# Use Ubuntu 24.04 as base image
FROM ubuntu:24.04
# Update package lists and install Apache2
RUN apt-get update && \
apt-get install -y apache2 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Configure Apache to listen on port 5004
RUN sed -i 's/Listen 80/Listen 5004/g' /etc/apache2/ports.conf && \
sed -i 's/<VirtualHost \*:80>/<VirtualHost *:5004>/g' /etc/apache2/sites-available/000-default.conf
# Expose port 5004
EXPOSE 5004
# Start Apache in foreground
CMD ["apache2ctl", "-D", "FOREGROUND"]
EOF
Dockerfile created! π
Explanation of each instruction:
FROM ubuntu:24.04
# Base image: Ubuntu 24.04 LTS
# Latest stable Ubuntu release
# Provides foundation for our image
RUN apt-get update && \
apt-get install -y apache2 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Update package lists
# Install apache2 package
# Clean up apt cache (reduce image size)
# Remove package lists (reduce image size)
RUN sed -i 's/Listen 80/Listen 5004/g' /etc/apache2/ports.conf && \
sed -i 's/<VirtualHost \*:80>/<VirtualHost *:5004>/g' /etc/apache2/sites-available/000-default.conf
# Configure Apache port
# Change default 80 to 5004 in ports.conf
# Change VirtualHost port to 5004
EXPOSE 5004
# Document that container uses port 5004
# Metadata only (doesn't actually publish port)
CMD ["apache2ctl", "-D", "FOREGROUND"]
# Default command when container starts
# Runs Apache in foreground (keeps container alive)
# -D FOREGROUND prevents Apache from daemonizing
Step 2.3: Verify Dockerfile Contents
# Display Dockerfile
cat /opt/docker/Dockerfile
Expected output:
# Use Ubuntu 24.04 as base image
FROM ubuntu:24.04
# Update package lists and install Apache2
RUN apt-get update && \
apt-get install -y apache2 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Configure Apache to listen on port 5004
RUN sed -i 's/Listen 80/Listen 5004/g' /etc/apache2/ports.conf && \
sed -i 's/<VirtualHost \*:80>/<VirtualHost *:5004>/g' /etc/apache2/sites-available/000-default.conf
# Expose port 5004
EXPOSE 5004
# Start Apache in foreground
CMD ["apache2ctl", "-D", "FOREGROUND"]
Dockerfile correct! β
Step 2.4: Check File Name (Critical!)
# Verify filename has capital D
ls -la /opt/docker/ | grep Dockerfile
Expected output:
-rw-r--r-- 1 root root 456 Dec 12 10:05 Dockerfile
Capital 'D' confirmed! β
Important: Docker looks for Dockerfile (capital D), not dockerfile
Phase 3: Build Docker Image
Step 3.1: Pull Base Image First (Optional)
# Pre-pull ubuntu:24.04 to verify availability
sudo docker pull ubuntu:24.04
Expected output:
24.04: Pulling from library/ubuntu
01007420e9b0: Pull complete
Digest: sha256:e3a4...
Status: Downloaded newer image for ubuntu:24.04
docker.io/library/ubuntu:24.04
Base image available! β
Step 3.2: Build Image from Dockerfile
# Build image from /opt/docker/Dockerfile
sudo docker build -t apache-custom /opt/docker/
Expected output:
[+] Building 45.2s (8/8) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 456B
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [internal] load metadata for docker.io/library/ubuntu:24.04
=> [1/4] FROM docker.io/library/ubuntu:24.04
=> => resolve docker.io/library/ubuntu:24.04
=> => sha256:e3a4... 1.13kB / 1.13kB
=> => sha256:01007... 29.54MB / 29.54MB
=> => extracting sha256:01007...
=> [2/4] RUN apt-get update && apt-get install -y apache2 && apt-get clean && rm -rf /var/lib/apt/lists/*
=> [3/4] RUN sed -i 's/Listen 80/Listen 5004/g' /etc/apache2/ports.conf && sed -i 's/<VirtualHost \*:80>/<VirtualHost *:5004>/g' /etc/apache2/sites-available/000-default.conf
=> [4/4] EXPOSE 5004
=> exporting to image
=> => exporting layers
=> => writing image sha256:abc123...
=> => naming to docker.io/library/apache-custom
Build successful! π
What happened during build:
Loaded Dockerfile
Pulled ubuntu:24.04 base image
Ran apt-get update and install apache2
Configured Apache for port 5004
Added EXPOSE metadata
Set CMD for starting Apache
Created final image
Step 3.3: Alternative - Build Without Tag
# Build without explicit tag (creates <none> tag)
sudo docker build /opt/docker/
# Or build with tag
sudo docker build -t myimage:v1 /opt/docker/
Both methods work! β
Phase 4: Verify Image Creation
Step 4.1: List Docker Images
# View all images
sudo docker images
Expected output:
REPOSITORY TAG IMAGE ID CREATED SIZE
apache-custom latest abc123def456 2 minutes ago 102MB
ubuntu 24.04 3b418d7b466a 2 weeks ago 77.8MB
Image created successfully! β
Step 4.2: Inspect Image
# View detailed image information
sudo docker inspect apache-custom
Shows:
Image ID
Creation timestamp
Size
Layers
Exposed ports
CMD configuration
Step 4.3: Check Image History
# View build layers
sudo docker history apache-custom
Expected output:
IMAGE CREATED CREATED BY SIZE
abc123def456 3 minutes ago CMD ["apache2ctl" "-D" "FOREGROUND"] 0B
def456ghi789 3 minutes ago EXPOSE 5004 0B
ghi789jkl012 3 minutes ago RUN sed -i 's/Listen 80/Listen 5004/g' /etc... 1.23kB
jkl012mno345 4 minutes ago RUN apt-get update && apt-get install -y ap... 25.3MB
3b418d7b466a 2 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ADD file:abc... in / 77.8MB
Shows all layers created! β
Step 4.4: Verify Exposed Port
# Check EXPOSE directive
sudo docker inspect apache-custom --format='{{.Config.ExposedPorts}}'
Expected output:
map[5004/tcp:{}]
Port 5004 exposed! β
Phase 5: Test Built Image
Step 5.1: Run Container from Image
# Create container from our image
sudo docker run -d --name test-apache -p 8080:5004 apache-custom
Expected output:
e1f2g3h4i5j6k7l8m9n0o1p2q3r4s5t6u7v8w9x0y1z2a3b4c5d6e7f8g9h0i1j2
Container running! β
Command explanation:
-d= detached mode (background)--name test-apache= container name-p 8080:5004= map host port 8080 to container port 5004apache-custom= our built image
Step 5.2: Verify Container Running
# Check container status
sudo docker ps
Expected output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e1f2g3h4i5j6 apache-custom "apache2ctl -D FOREGβ¦" 30 seconds ago Up 28 seconds 0.0.0.0:8080->5004/tcp test-apache
Container active! β
Step 5.3: Check Apache Process
# View processes in container
sudo docker exec test-apache ps aux | grep apache
Expected output:
root 1 0.0 0.3 12345 12345 ? Ss 10:00 0:00 /usr/sbin/apache2 -D FOREGROUND
www-data 7 0.0 0.2 12345 12345 ? S 10:00 0:00 /usr/sbin/apache2 -D FOREGROUND
www-data 8 0.0 0.2 12345 12345 ? S 10:00 0:00 /usr/sbin/apache2 -D FOREGROUND
Apache running in foreground! β
Step 5.4: Verify Port Inside Container
# Check Apache listening on port 5004
sudo docker exec test-apache netstat -tulpn | grep 5004
Expected output:
tcp 0 0 0.0.0.0:5004 0.0.0.0:* LISTEN 1/apache2
tcp6 0 0 :::5004 :::* LISTEN 1/apache2
Apache listening on port 5004! β
Step 5.5: Test HTTP Response
# Access Apache from host
curl -I http://localhost:8080
Expected output:
HTTP/1.1 200 OK
Date: Thu, 12 Dec 2025 10:00:00 GMT
Server: Apache/2.4.58 (Ubuntu)
Last-Modified: Thu, 12 Dec 2025 09:55:00 GMT
Content-Type: text/html
HTTP 200 - Apache serving! β
Step 5.6: Get Full Page
# Retrieve default Apache page
curl http://localhost:8080 | head -10
Shows Ubuntu Apache default page! β
Phase 6: Cleanup and Final Verification
Step 6.1: Stop Test Container
# Stop and remove test container
sudo docker stop test-apache
sudo docker rm test-apache
Test container removed! β
Step 6.2: Verify Dockerfile Still Exists
# Confirm Dockerfile in place
ls -la /opt/docker/Dockerfile
Expected output:
-rw-r--r-- 1 root root 456 Dec 12 10:05 /opt/docker/Dockerfile
Dockerfile present! β
Step 6.3: Verify Image Remains
# Image persists after container removal
sudo docker images | grep apache-custom
Expected output:
apache-custom latest abc123def456 15 minutes ago 102MB
Image available for reuse! β
Step 6.4: Test Image Reusability
# Can create new containers from same image
sudo docker run -d --name apache-prod -p 5004:5004 apache-custom
sudo docker ps | grep apache-prod
New container created instantly! β
Step 6.5: Final Verification Summary
# Complete verification
echo "=== Dockerfile Build Verification ==="
echo ""
echo "1. Dockerfile Location:"
ls /opt/docker/Dockerfile && echo " β /opt/docker/Dockerfile exists"
echo ""
echo "2. Image Created:"
sudo docker images apache-custom --format " β {{.Repository}}:{{.Tag}} ({{.Size}})"
echo ""
echo "3. Base Image:"
sudo docker inspect apache-custom --format ' β Base: {{(index .Config.Image)}}' 2>/dev/null || echo " β Base: ubuntu:24.04 (from FROM instruction)"
echo ""
echo "4. Exposed Port:"
sudo docker inspect apache-custom --format=' β Port: {{range $k, $v := .Config.ExposedPorts}}{{$k}}{{end}}'
echo ""
echo "5. Default Command:"
sudo docker inspect apache-custom --format=' β CMD: {{.Config.Cmd}}'
echo ""
echo "Status: β DOCKERFILE BUILD COMPLETE"
Expected output:
=== Dockerfile Build Verification ===
1. Dockerfile Location:
β /opt/docker/Dockerfile exists
2. Image Created:
β apache-custom:latest (102MB)
3. Base Image:
β Base: ubuntu:24.04 (from FROM instruction)
4. Exposed Port:
β Port: 5004/tcp
5. Default Command:
β CMD: [apache2ctl -D FOREGROUND]
Status: β DOCKERFILE BUILD COMPLETE
ALL REQUIREMENTS MET! ππππ
TASK COMPLETE - CUSTOM IMAGE BUILT FROM DOCKERFILE! π
π Understanding What We Accomplished
Dockerfile Build Process
What happened during docker build:
Step 1: Parse Dockerfile
ββ Docker reads /opt/docker/Dockerfile
ββ Validates syntax
ββ Plans build steps
Step 2: Execute FROM
ββ Pulls ubuntu:24.04 if not local
ββ Creates base layer
ββ Sets up build environment
Step 3: Execute RUN (apt-get)
ββ Starts temporary container
ββ Runs: apt-get update
ββ Runs: apt-get install apache2
ββ Runs: apt-get clean
ββ Commits layer
ββ Removes temporary container
Step 4: Execute RUN (sed)
ββ Starts new temporary container
ββ Modifies ports.conf
ββ Modifies 000-default.conf
ββ Commits layer
ββ Removes temporary container
Step 5: Execute EXPOSE
ββ Adds metadata
ββ No filesystem change
Step 6: Execute CMD
ββ Sets default command
ββ Adds metadata
ββ Final image created
Result: Image with 4 layers + base
Layer Caching
Docker caches layers for efficiency:
First build:
[1/4] FROM ubuntu:24.04 5.2s
[2/4] RUN apt-get update... 38.1s β Slow
[3/4] RUN sed -i... 0.3s
[4/4] EXPOSE 5004 0.1s
Total: 45.2s
Second build (no changes):
[1/4] FROM ubuntu:24.04 CACHED
[2/4] RUN apt-get update... CACHED β Fast!
[3/4] RUN sed -i... CACHED
[4/4] EXPOSE 5004 CACHED
Total: 0.8s
Build with change to layer 3:
[1/4] FROM ubuntu:24.04 CACHED
[2/4] RUN apt-get update... CACHED
[3/4] RUN sed -i... 2.1s β Rebuilt
[4/4] EXPOSE 5004 0.1s β Rebuilt
Total: 3.0s
Cache invalidated from changed layer forward!
Best Practices Applied
Our Dockerfile follows best practices:
# 1. Specific base image version
FROM ubuntu:24.04 # Not ubuntu:latest
# 2. Combined RUN commands (fewer layers)
RUN apt-get update && \
apt-get install -y apache2 && \
apt-get clean
# 3. Cleanup in same layer (smaller image)
RUN ... && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 4. Configuration as code (reproducible)
RUN sed -i 's/Listen 80/Listen 5004/g' ...
# 5. Documented ports
EXPOSE 5004
# 6. Foreground process (container stays alive)
CMD ["apache2ctl", "-D", "FOREGROUND"]
π‘ Key Takeaways
β¨ Dockerfile is text file with build instructions β¨ FROM specifies base image (must be first) β¨ RUN executes commands during build β¨ Each instruction creates layer in image β¨ EXPOSE documents ports (metadata only) β¨ CMD sets default command when container starts β¨ docker build creates image from Dockerfile β¨ Layers are cached for faster rebuilds β¨ Capital 'D' in Dockerfile (Docker convention) β¨ Reproducible builds - same Dockerfile = same image
π Interview Questions
Q1: Explain the difference between RUN, CMD, and ENTRYPOINT in a Dockerfile.
Answer: Three distinct purposes in container lifecycle. RUN: Executes during image BUILD, creates new layer in image, used for installing software, multiple RUN commands allowed. Example: RUN apt-get install apache2 runs at build time. CMD: Provides DEFAULT command when container STARTS, executed at runtime, can be overridden by docker run arguments, only last CMD takes effect. Example: CMD ["apache2ctl", "-D", "FOREGROUND"] starts Apache. ENTRYPOINT: Configures container as EXECUTABLE, always executes at runtime, arguments passed to it (not replaced), combines with CMD for default args. Example: ENTRYPOINT ["python"] with CMD ["app.py"] runs python app.py. Combined usage: dockerfile ENTRYPOINT ["apache2ctl"] CMD ["-D", "FOREGROUND"] # docker run image β apache2ctl -D FOREGROUND # docker run image -V β apache2ctl -V (CMD overridden) Best practice: RUN for build-time commands, CMD for default runtime behavior, ENTRYPOINT when container is single-purpose executable.
Q2: Why do we combine commands with && in RUN instructions instead of using multiple RUN commands?
Answer: Layer optimization and image size reduction. Multiple RUN commands: dockerfile RUN apt-get update RUN apt-get install -y apache2 RUN apt-get clean Creates 3 separate layers, each RUN commits a layer, cache files persist across layers, larger final image. Example: Layer 1: update (adds cache), Layer 2: install (adds packages + cache), Layer 3: clean (removes cache but previous layers still have it). Combined with &&: dockerfile RUN apt-get update && \ apt-get install -y apache2 && \ apt-get clean Single layer created, cleanup happens in same layer, cache removed before commit, smaller image size. Size comparison: Multiple RUN: 150 MB (cache in multiple layers), Combined RUN: 105 MB (cache cleaned in one layer). Cache invalidation: Changing any RUN rebuilds that layer and all following, combined commands = fewer cache invalidations. Best practice: Group related commands, clean up in same RUN, order commands by change frequency (least to most).
Q3: What is layer caching in Docker builds and how can you optimize it?
Answer: Docker caches each instruction's result for faster rebuilds. How caching works: Each instruction creates layer with unique hash, Docker checks if instruction + context changed, if unchanged, uses cached layer instead of rebuilding, cache used until first changed layer. Example flow: dockerfile FROM ubuntu:24.04 # Layer 1 RUN apt-get update # Layer 2 COPY requirements.txt . # Layer 3 β Changed! RUN pip install -r requirements.txt # Layer 4 COPY . . # Layer 5 If requirements.txt changes: Layers 1-2 use cache (fast), Layer 3 rebuilt (changed file), Layers 4-5 rebuilt (invalidated). Optimization strategies: 1) Order by change frequency: FROM (never changes), RUN apt-get (rarely), COPY requirements (sometimes), COPY source code (often). 2) Separate dependencies: dockerfile COPY requirements.txt . RUN pip install -r requirements.txt COPY . . # Code changes don't rebuild dependencies 3) Use .dockerignore: Exclude files that shouldn't trigger rebuilds (.git, *.md). 4) Multi-stage builds: Separate build and runtime stages. Cache invalidation triggers: File content change, file timestamp change, instruction text change. Best practice: Put frequently changing instructions last, copy dependency files before source code.
Q4: How would you reduce the size of a Docker image?
Answer: Multiple strategies for lean images. 1) Use smaller base images: dockerfile # Bad: FROM ubuntu:24.04 # 77 MB # Better: FROM ubuntu:24.04-slim # 30 MB # Best: FROM alpine:latest # 5 MB 2) Multi-stage builds: dockerfile FROM golang:1.21 AS builder WORKDIR /app COPY . . RUN go build -o app # Build with full tools FROM alpine:latest COPY --from=builder /app/app / # Only copy binary CMD ["/app"] # Final image: 15 MB vs 1 GB! 3) Clean up in same layer: dockerfile # Bad (cache persists): RUN apt-get update RUN apt-get install -y apache2 RUN apt-get clean # Still 150 MB # Good (cleanup same layer): RUN apt-get update && \ apt-get install -y apache2 && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # 105 MB 4) Remove unnecessary files: dockerfile RUN wget https://example.com/file.tar.gz && \ tar xzf file.tar.gz && \ rm file.tar.gz # Remove after extraction 5) Use .dockerignore: Exclude: .git/, node_modules/, *.log, documentation. 6) Combine COPY commands: One COPY instead of multiple reduces layers. 7) Don't install recommended packages: apt-get install -y --no-install-recommends. Real example: Before: 850 MB, After: 120 MB (85% reduction).
Q5: What's the difference between COPY and ADD in Dockerfiles?
Answer: Both copy files but ADD has extra features. COPY: Simple file/directory copy, source from build context, destination in image, predictable behavior. Syntax: COPY source dest. Example: COPY app.py /app/. ADD: Everything COPY does PLUS auto-extraction and URL download, can extract tar archives automatically, can download from URLs. Syntax: ADD source dest. Key differences: dockerfile # COPY behavior COPY archive.tar.gz /app/ # Copies as-is (file) # ADD behavior ADD archive.tar.gz /app/ # Auto-extracts to /app/ ADD http://example.com/file /app/ # Downloads file Problems with ADD: Less predictable (auto-extraction sometimes unwanted), can download files (security concern), breaks caching more easily. When to use each: Use COPY (default choice): Local files/directories, want explicit behavior, better caching. Example: COPY requirements.txt .. Use ADD: Need auto-extraction of archives, need URL download (rare). Example: ADD rootfs.tar.gz /. Best practice: Always use COPY unless specifically need ADD features, more explicit and predictable, better for maintainability. Docker official recommendation: Prefer COPY.
Q6: How do you debug a failed Docker build?
Answer: Systematic debugging approach. Step 1: Read error message carefully: bash docker build -t myimage . # Error at step 3/5: RUN apt-get install typo-package # E: Unable to locate package typo-package Identifies which instruction failed. Step 2: Build up to failing layer: dockerfile # Original failing Dockerfile FROM ubuntu:24.04 RUN apt-get update RUN apt-get install -y typo-package # Fails here # Debug version FROM ubuntu:24.04 RUN apt-get update # Stop before failure, build this Build and run: docker build -t debug . && docker run -it debug bash. Step 3: Manually test command: Inside container: bash apt-get install typo-package # Test failing command apt-cache search typo # Find correct package name Step 4: Use --progress=plain: bash docker build --progress=plain -t myimage . # Shows full output, not abbreviated Step 5: Inspect intermediate images: Docker keeps intermediate images, run them: bash docker images # Find <none> images docker run -it <image-id> bash Step 6: Add debug output: dockerfile RUN echo "Debug: Installing packages" && \ apt-get update && \ echo "Debug: Update complete" && \ apt-get install -y apache2 && \ echo "Debug: Install complete" Step 7: Check BuildKit cache: bash docker builder prune # Clear build cache docker build --no-cache -t myimage . # Force rebuild Common issues: Typos in package names, missing dependencies, network problems, permission issues, context files not found. Best practice: Test commands in running container first, use specific versions to avoid changes, check Docker Hub for similar Dockerfiles.
π Dockerfile Best Practices
1. Order instructions by change frequency:
# Things that change rarely (top)
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y apache2
# Things that change sometimes
COPY requirements.txt .
RUN pip install -r requirements.txt
# Things that change often (bottom)
COPY . .
2. Use specific image versions:
# Bad (unpredictable)
FROM ubuntu:latest
# Good (reproducible)
FROM ubuntu:24.04
# Better (specific digest)
FROM ubuntu@sha256:abcd1234...
3. Minimize layer count:
# Bad (many layers)
RUN apt-get update
RUN apt-get install -y apache2
RUN apt-get install -y curl
RUN apt-get clean
# Good (one layer)
RUN apt-get update && \
apt-get install -y \
apache2 \
curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
4. Use .dockerignore:
# .dockerignore file
.git
.gitignore
*.md
.env
node_modules
*.log
.DS_Store
5. Don't run as root:
# Create non-root user
RUN useradd -m -s /bin/bash appuser
# Switch to non-root
USER appuser
# Now commands run as appuser
CMD ["python", "app.py"]
6. Use HEALTHCHECK:
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:5004/ || exit 1
7. Label your images:
LABEL maintainer="devops@nautilus.com"
LABEL version="1.0"
LABEL description="Custom Apache on port 5004"
π Real-World Dockerfile Examples
Example 1: Python Flask Application
FROM python:3.11-slim
WORKDIR /app
# Copy and install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 5000
CMD ["python", "app.py"]
Example 2: Node.js Application
FROM node:18-alpine
WORKDIR /usr/src/app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application
COPY . .
# Non-root user
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Example 3: Multi-Stage Build (Go)
# Build stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp
# Runtime stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]
Example 4: Nginx with Custom Config
FROM nginx:alpine
# Remove default config
RUN rm /etc/nginx/conf.d/default.conf
# Copy custom config
COPY nginx.conf /etc/nginx/conf.d/
# Copy static files
COPY html/ /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Example 5: Database with Initialization
FROM postgres:15-alpine
# Set environment variables
ENV POSTGRES_DB=myapp
ENV POSTGRES_USER=appuser
ENV POSTGRES_PASSWORD=secret
# Copy initialization scripts
COPY init-scripts/ /docker-entrypoint-initdb.d/
EXPOSE 5432
π Dockerfile vs Docker Compose
When to use each:
Dockerfile:
β Defines single image
β How to build the image
β Application dependencies
β Configuration
Docker Compose:
β Defines multiple services
β How services interact
β Network configuration
β Volume management
Example workflow:
1. Write Dockerfile (how to build)
2. Build image: docker build -t myapp .
3. Write docker-compose.yml (orchestration)
4. Run: docker-compose up
Both together = complete application stack
π― Common Dockerfile Patterns
Pattern 1: Environment Variables
FROM ubuntu:24.04
# Set environment variables
ENV APP_HOME=/app \
APP_USER=appuser \
DEBIAN_FRONTEND=noninteractive
WORKDIR $APP_HOME
RUN apt-get update && \
apt-get install -y apache2
USER $APP_USER
Pattern 2: ARG for Build-Time Variables
FROM ubuntu:24.04
# Build arguments
ARG APACHE_VERSION=2.4.58
ARG BUILD_DATE
ARG VCS_REF
# Labels using ARGs
LABEL build_date=$BUILD_DATE
LABEL vcs_ref=$VCS_REF
RUN apt-get update && apt-get install -y apache2
# Build with: docker build --build-arg BUILD_DATE=$(date) .
Pattern 3: Conditional Execution
FROM ubuntu:24.04
ARG ENABLE_CACHE=true
RUN apt-get update && \
apt-get install -y apache2 && \
if [ "$ENABLE_CACHE" = "false" ]; then \
rm -rf /var/lib/apt/lists/*; \
fi
Pattern 4: Volume Declaration
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y apache2
# Create directories for volumes
RUN mkdir -p /var/log/apache2 /var/www/html
# Declare volumes
VOLUME ["/var/log/apache2", "/var/www/html"]
CMD ["apache2ctl", "-D", "FOREGROUND"]
Pattern 5: Signal Handling
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y apache2
# Stop signal for graceful shutdown
STOPSIGNAL SIGTERM
CMD ["apache2ctl", "-D", "FOREGROUND"]
π§ Troubleshooting Common Issues
Issue 1: Build context too large
# Problem: Sending build context to Docker daemon: 2.5 GB
# Solution: Create .dockerignore
cat > .dockerignore << EOF
.git
node_modules
*.log
.env
EOF
# Now: Sending build context: 50 MB
Issue 2: Layer caching not working
# Problem: Every build takes 10 minutes
# Check if files changed unnecessarily
# Solution: Touch only changed files
# Use .dockerignore to exclude logs, temp files
Issue 3: Permission denied errors
# Problem: Can't write to /app directory
# Solution: Set proper ownership
FROM ubuntu:24.04
RUN useradd -m appuser
RUN mkdir /app && chown appuser:appuser /app
USER appuser
WORKDIR /app
Issue 4: CMD not executing
# Problem: Container exits immediately
# Bad: CMD runs in shell, exits when done
CMD apache2ctl start
# Good: Foreground process
CMD ["apache2ctl", "-D", "FOREGROUND"]
Issue 5: Image too large
# Check image size
docker images myapp
# Analyze layers
docker history myapp
# Solution: Multi-stage build or alpine base
π Performance Optimization
Optimize build time:
# 1. Use build cache effectively
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y apache2 # Cached
# 2. Parallel builds (BuildKit)
# export DOCKER_BUILDKIT=1
# 3. Use --cache-from
docker build --cache-from myapp:latest -t myapp:new .
# 4. Order by change frequency
# Dependencies first, code last
Optimize image size:
# Multi-stage build
FROM ubuntu:24.04 AS builder
RUN apt-get update && apt-get install -y build-essential
COPY src/ .
RUN make build
FROM ubuntu:24.04-slim
COPY --from=builder /app/binary /app/
CMD ["/app/binary"]
Optimize runtime:
# Use minimal base
FROM alpine:latest # 5 MB vs Ubuntu 77 MB
# Remove unnecessary tools
RUN apk add --no-cache apache2
# Use specific versions (predictable)
FROM alpine:3.19
π Final Thoughts
You've successfully mastered Dockerfile creation! This is the foundation of modern container deployment:
What you accomplished:
β Created Dockerfile at /opt/docker/Dockerfile
β Used ubuntu:24.04 as base image
β Installed and configured Apache2
β Set custom port 5004
β Built functional Docker image
β Verified image works correctly
Real-world impact:
Reproducible builds: Same Dockerfile = same image every time
Documentation: Dockerfile is self-documenting
Version control: Track changes in Git
CI/CD integration: Automated image builds
Team collaboration: Share build process
Infrastructure as code: Declarative image definition
Key lessons learned:
Dockerfile is text file with build instructions
Each instruction creates a layer
Order matters for caching
Combine commands to reduce layers
Clean up in same RUN instruction
Use specific base image versions
Keep images small and secure
Test during development
Best practices applied:
Specific Ubuntu version (24.04, not latest)
Combined RUN commands (fewer layers)
Cleanup in same layer (smaller image)
Configuration as code (sed commands)
Foreground process (CMD with -D FOREGROUND)
Documented port (EXPOSE 5004)
This is production container deployment! Every containerized application starts with a well-crafted Dockerfile! πͺ
π What's Next?
Day 41 complete! π You've mastered Dockerfile creation and image building!
Skills Mastered Today:
β Writing production Dockerfiles
β Image layer optimization
β Apache configuration in containers
β Docker build process
β Image verification and testing
Coming up: More Docker adventures - Docker Compose for multi-container apps, container orchestration, advanced networking!
Day: 41/100
Challenge: KodeKloud Cloud DevOps
Date: December 16, 2025
Topic: Dockerfile Creation and Image Building
How do you structure your Dockerfiles? What's your image optimization strategy? Share your Dockerfile best practices! ποΈ




