Skip to main content

Command Palette

Search for a command to run...

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

Published
β€’24 min read
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:

  1. Loaded Dockerfile

  2. Pulled ubuntu:24.04 base image

  3. Ran apt-get update and install apache2

  4. Configured Apache for port 5004

  5. Added EXPOSE metadata

  6. Set CMD for starting Apache

  7. 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 5004

  • apache-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! πŸ—οΈ

More from this blog

πŸš€ DevOps Challenge- KodeKloud Solutions

73 posts