Creating Your Own Images (Dockerfile)

Now we'll learn how to create your own custom Docker images using Dockerfiles. This is where Docker becomes really powerful!


Part 1: What is a Dockerfile?

Simple Definition

Dockerfile = A text file containing instructions to build a Docker image.

Think of it as:

Recipe Card (Dockerfile):
├── List ingredients (base image)
├── Preparation steps (RUN commands)
├── Add your items (COPY files)
├── Cooking instructions (CMD)
└── Final dish (your custom image)

Following recipe → Creates the dish
Reading Dockerfile → Builds the image

Another Analogy:

Construction Blueprint (Dockerfile):
├── Foundation type (FROM)
├── Building materials (RUN install packages)
├── Interior design (COPY your files)
├── Final touches (CMD)
└── Complete building (custom image)

Why Create Custom Images?

Instead of using existing images, you create custom ones to:

1. Package your own application
   └── Your code + environment together

2. Customize existing images
   └── Add tools/packages you need

3. Create reproducible environments
   └── Same setup everywhere

4. Share with team
   └── Everyone uses same environment

5. Deploy applications
   └── Production-ready packages

Dockerfile Basics

A Dockerfile is just a text file named Dockerfile (no extension).

Simple example:

FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y python3
COPY app.py /app/
CMD ["python3", "/app/app.py"]

What this does:

Line 1: Start with Ubuntu 22.04 as base
Line 2: Update package lists
Line 3: Install Python 3
Line 4: Copy your app.py file into image
Line 5: Run your app when container starts

Part 2: Dockerfile Instructions - FROM

FROM - The Base Image

FROM = Starting point for your image

Syntax:

FROM image:tag

Examples:

# Start with Ubuntu
FROM ubuntu:22.04

# Start with Python already installed
FROM python:3.11

# Start with Node.js
FROM node:18

# Start with minimal Alpine Linux
FROM alpine:3.18

# Start from scratch (empty image)
FROM scratch

Understanding FROM

Every Dockerfile MUST start with FROM:

FROM ubuntu:22.04
# ↑
# This is always the first instruction
# (except for ARG, which we'll learn later)

Why use different base images?

Choose based on needs:

Need Python?
FROM python:3.11
└── Python already installed ✓

Need Node.js?
FROM node:18
└── Node.js already installed ✓

Want minimal size?
FROM alpine:3.18
└── Smallest base (5MB) ✓

Want full Ubuntu?
FROM ubuntu:22.04
└── More packages available ✓

Starting from scratch?
FROM scratch
└── Build everything yourself

Example: Different Base Images

Example 1: Start with Ubuntu, install Python

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3
# Manual installation

Example 2: Start with Python already

FROM python:3.11
# Python already included! ✓

Which is better?

Option 2 (FROM python:3.11) because:
├── Python pre-configured correctly
├── Includes pip and other tools
├── Follows best practices
└── Less code to write

Unless you need specific Ubuntu features,
use official language images! ✓

Part 3: Dockerfile Instructions - RUN

RUN - Execute Commands During Build

RUN = Run commands when building the image

Syntax:

RUN command

Examples:

# Install packages (Ubuntu/Debian)
RUN apt-get update && apt-get install -y curl

# Install packages (Alpine)
RUN apk add --no-cache curl

# Install Python packages
RUN pip install flask

# Create directories
RUN mkdir -p /app/data

# Download files
RUN curl -O https://example.com/file.zip

RUN - Important Concepts

Each RUN creates a new layer:

FROM ubuntu:22.04
RUN apt-get update           # Layer 1
RUN apt-get install -y curl  # Layer 2
RUN apt-get install -y vim   # Layer 3

Better approach - combine commands:

FROM ubuntu:22.04
RUN apt-get update && \
    apt-get install -y curl vim
# Single layer ✓

Why combine?

Separate RUN commands:
├── Layer 1: apt-get update (50MB)
├── Layer 2: install curl (10MB)
├── Layer 3: install vim (30MB)
└── Total: 90MB + overhead

Combined RUN:
└── Layer 1: update + installs (60MB)
└── Total: 60MB (smaller!) ✓

RUN Examples for Different Languages

Python:

FROM python:3.11
RUN pip install flask sqlalchemy requests

Node.js:

FROM node:18
RUN npm install express mongoose

System packages:

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
    git \
    curl \
    vim \
    && rm -rf /var/lib/apt/lists/*
# ↑ Cleanup to reduce size

Part 4: Dockerfile Instructions - COPY and ADD

COPY - Copy Files from Host to Image

COPY = Copy files from your computer into the image

Syntax:

COPY source destination

Examples:

# Copy single file
COPY app.py /app/

# Copy all files in current directory
COPY . /app/

# Copy multiple files
COPY app.py config.json /app/

# Copy directory
COPY ./src /app/src/

Understanding COPY

Visual:

Your Computer:              Docker Image:
┌─────────────────┐        ┌──────────────┐
│ my-project/     │        │              │
│ ├── app.py      │  COPY  │ /app/        │
│ ├── config.json │───────→│ ├── app.py   │
│ └── data/       │        │ └── config   │
└─────────────────┘        └──────────────┘

Example Dockerfile:

FROM python:3.11

# Create app directory in image
RUN mkdir /app

# Copy your files into image
COPY app.py /app/
COPY requirements.txt /app/

# Copy everything
COPY . /app/

ADD vs COPY

ADD = Like COPY but with extra features

# COPY: Just copies files
COPY app.py /app/

# ADD: Copies AND extracts archives
ADD archive.tar.gz /app/
# ↑ Automatically extracts!

# ADD: Can download from URL
ADD https://example.com/file.txt /app/

Which to use?

Use COPY (recommended):
├── Simpler
├── More predictable
└── Best practice ✓

Use ADD only when:
├── Need to extract archives
└── Need to download from URL

Docker best practice: Prefer COPY

Part 5: Dockerfile Instructions - WORKDIR

WORKDIR - Set Working Directory

WORKDIR = Set the current directory inside the image

Syntax:

WORKDIR /path/to/directory

Without WORKDIR:

FROM ubuntu:22.04
RUN mkdir /app
COPY app.py /app/
RUN cd /app && python3 app.py
# ↑ Must keep specifying /app

With WORKDIR:

FROM ubuntu:22.04
WORKDIR /app
# ↑ Set once

COPY app.py .
# . means current directory (/app)

RUN python3 app.py
# Already in /app directory

WORKDIR Benefits

Makes Dockerfile cleaner:

FROM python:3.11

# Set working directory
WORKDIR /app

# Now all commands run in /app
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

CMD ["python", "app.py"]
# Runs from /app directory automatically

WORKDIR creates directory if it doesn't exist:

FROM ubuntu:22.04
WORKDIR /app/data/logs
# ↑ Creates entire path automatically ✓

Part 6: Dockerfile Instructions - CMD and ENTRYPOINT

CMD - Default Command

CMD = Command to run when container starts

Syntax:

CMD ["executable", "param1", "param2"]

Examples:

# Run Python script
CMD ["python3", "app.py"]

# Start web server
CMD ["nginx", "-g", "daemon off;"]

# Run shell command
CMD ["echo", "Hello Docker!"]

Understanding CMD

CMD sets the default command:

FROM ubuntu:22.04
WORKDIR /app
COPY hello.py .
CMD ["python3", "hello.py"]

When you run container:

docker run myimage
# Automatically runs: python3 hello.py

Can be overridden:

docker run myimage echo "Different command"
# Runs: echo "Different command"
# (CMD is ignored)

CMD Formats

Three formats:

1. Exec form (recommended):

CMD ["python3", "app.py"]
# ↑ As JSON array

2. Shell form:

CMD python3 app.py
# ↑ As shell command

3. As parameters to ENTRYPOINT:

ENTRYPOINT ["python3"]
CMD ["app.py"]
# Together: python3 app.py

ENTRYPOINT - Fixed Command

ENTRYPOINT = Command that always runs (cannot be easily overridden)

Difference between CMD and ENTRYPOINT:

# Using CMD
CMD ["python3", "app.py"]

docker run myimage
# Runs: python3 app.py

docker run myimage ls
# Runs: ls (CMD ignored!)
# Using ENTRYPOINT
ENTRYPOINT ["python3", "app.py"]

docker run myimage
# Runs: python3 app.py

docker run myimage ls
# Runs: python3 app.py ls
#       ↑ Still runs ENTRYPOINT!

CMD + ENTRYPOINT Together

Powerful combination:

FROM python:3.11
WORKDIR /app
COPY app.py .

ENTRYPOINT ["python3"]
CMD ["app.py"]

Usage:

# Run default
docker run myimage
# Executes: python3 app.py

# Run different script
docker run myimage test.py
# Executes: python3 test.py
# ↑ ENTRYPOINT fixed, CMD replaced

Part 7: Dockerfile Instructions - ENV

ENV - Environment Variables

ENV = Set environment variables in the image

Syntax:

ENV KEY=VALUE

Examples:

# Set single variable
ENV APP_ENV=production

# Set multiple variables
ENV APP_ENV=production \
    DB_HOST=localhost \
    DB_PORT=5432

Using ENV

Example Dockerfile:

FROM python:3.11
WORKDIR /app

# Set environment variables
ENV PYTHONUNBUFFERED=1
ENV APP_PORT=8000
ENV DEBUG=False

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

In your Python code (app.py):

import os

port = os.getenv('APP_PORT')  # Gets 8000
debug = os.getenv('DEBUG')     # Gets False
print(f"Starting app on port {port}")

Override ENV at Runtime

You can override when running container:

docker run -e APP_PORT=9000 myimage
# APP_PORT is now 9000 (overrides Dockerfile ENV)

Part 8: Dockerfile Instructions - EXPOSE

EXPOSE - Document Which Ports are Used

EXPOSE = Tells Docker which ports the container listens on

Syntax:

EXPOSE port

Examples:

# Web server on port 80
EXPOSE 80

# Multiple ports
EXPOSE 80 443

# With protocol
EXPOSE 8080/tcp
EXPOSE 53/udp

Understanding EXPOSE

IMPORTANT: EXPOSE is just documentation!

EXPOSE does NOT:
├── Actually publish the port
├── Make port accessible from outside
└── Do port mapping

EXPOSE only:
└── Documents which ports app uses
└── Helps other developers understand

You still need -p when running:

docker run -p 8080:80 myimage
# ↑ This is what actually publishes the port

Example:

FROM nginx:latest
EXPOSE 80
# ↑ Documents: "nginx uses port 80"

Part 9: Creating Your First Dockerfile

Example 1: Simple Python Application

Let's create a real Dockerfile!

Step 1: Create project directory

mkdir my-python-app
cd my-python-app

Step 2: Create a simple Python app (app.py)

# app.py
print("Hello from Docker!")
print("This is my first containerized app!")

import time
while True:
    print("App is running...")
    time.sleep(5)

Step 3: Create Dockerfile

Create a file named Dockerfile (no extension):

# Use Python 3.11 as base image
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Copy Python script into container
COPY app.py .

# Run the application
CMD ["python", "app.py"]

Step 4: Build the image

docker build -t my-python-app .

Understanding the command:

docker build
    -t my-python-app    ← Tag (name) for the image
    .                   ← Build context (current directory)

What you'll see:

[+] Building 12.3s (8/8) FINISHED
 => [1/3] FROM python:3.11-slim
 => [2/3] WORKDIR /app
 => [3/3] COPY app.py .
 => exporting to image
 => => naming to my-python-app

Successfully built!

Step 5: Run your container

docker run my-python-app

Output:

Hello from Docker!
This is my first containerized app!
App is running...
App is running...
App is running...
...

🎉 Congratulations! You built your first Docker image!


Understanding the Build Process

What happened during docker build:

Step 1: Read Dockerfile
Step 2: FROM python:3.11-slim
        └── Download base image (if not present)

Step 3: WORKDIR /app
        └── Create /app directory in image

Step 4: COPY app.py .
        └── Copy your file into image

Step 5: CMD ["python", "app.py"]
        └── Set default command

Step 6: Create final image
        └── Tag it as "my-python-app"

Example 2: Python Web App with Dependencies

Let's create something more realistic!

Step 1: Create project structure

mkdir flask-app
cd flask-app

Step 2: Create app.py

# app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return '<h1>Hello from Dockerized Flask!</h1>'

@app.route('/about')
def about():
    return '<h1>This is a Flask app running in Docker</h1>'

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

Step 3: Create requirements.txt

Flask==3.0.0

Step 4: Create Dockerfile

# Use Python 3.11 slim image
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Copy requirements file
COPY requirements.txt .

# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY app.py .

# Expose port 5000
EXPOSE 5000

# Run the application
CMD ["python", "app.py"]

Step 5: Build the image

docker build -t flask-app .

Step 6: Run the container

docker run -d -p 5000:5000 --name my-flask-app flask-app

Step 7: Test it

Open browser and go to:

  • http://localhost:5000
  • http://localhost:5000/about

You should see your Flask app running! 🎉


Understanding This Dockerfile

Let's break it down:

FROM python:3.11-slim
# Start with lightweight Python image

WORKDIR /app
# All subsequent commands run in /app

COPY requirements.txt .
# Copy dependencies list first
# Why first? For layer caching! (explained next)

RUN pip install --no-cache-dir -r requirements.txt
# Install Python packages
# --no-cache-dir = Don't save pip cache (smaller image)

COPY app.py .
# Copy application code

EXPOSE 5000
# Document that Flask uses port 5000

CMD ["python", "app.py"]
# Start Flask when container runs

Build Context and .dockerignore

Build Context = Files Docker can access during build

When you run:

docker build -t myapp .
#                      ↑
#                 Build context (current directory)

Docker sends all files in this directory to Docker daemon:

my-project/
├── app.py              ← Sent to Docker
├── requirements.txt    ← Sent to Docker
├── data.csv            ← Sent to Docker
├── old_backup.zip      ← Sent to Docker (unnecessary!)
└── node_modules/       ← Sent to Docker (huge, unnecessary!)

Using .dockerignore

Create .dockerignore file to exclude files:

# .dockerignore

# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Git
.git/
.gitignore

# Documentation
*.md
docs/

# Tests
tests/
*.test.py

# Data files (if not needed in image)
*.csv
*.xlsx
data/

Now build is faster:

Without .dockerignore:
└── Sends 500MB to Docker

With .dockerignore:
└── Sends 5MB to Docker ✓

Build time: Much faster! ✓

Part 10: Building Images - Best Practices

Best Practice 1: Layer Caching

Docker caches layers to speed up builds!

Bad example (slow rebuilds):

FROM python:3.11-slim
WORKDIR /app

# Copy everything
COPY . .

# Install dependencies
RUN pip install -r requirements.txt

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

Problem:

Change app.py:
        ↓
COPY . . changes (includes app.py)
        ↓
Cache invalidated
        ↓
Must re-run pip install (slow!) ✗

Good example (fast rebuilds):

FROM python:3.11-slim
WORKDIR /app

# Copy only requirements first
COPY requirements.txt .

# Install dependencies
RUN pip install -r requirements.txt

# Copy application code
COPY app.py .

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

Why better:

Change app.py:
        ↓
COPY requirements.txt . (unchanged)
        ↓
RUN pip install (cached! ✓)
        ↓
COPY app.py . (only this layer rebuilds)
        ↓
Fast rebuild! ✓

Rule: Copy files that change less frequently first!


Best Practice 2: Minimize Layers

Combine RUN commands:

Bad:

RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get install -y git
# 4 layers ✗

Good:

RUN apt-get update && apt-get install -y \
    curl \
    vim \
    git
# 1 layer ✓

Best Practice 3: Clean Up in Same Layer

Bad:

RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# Cleanup in separate layer doesn't reduce image size! ✗

Good:

RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*
# Cleanup in same layer reduces size! ✓

Best Practice 4: Use Specific Tags

Bad:

FROM python:latest
# What version? Changes over time! ✗

Good:

FROM python:3.11-slim
# Specific version, predictable! ✓

Best Practice 5: Use .dockerignore

Always create .dockerignore to exclude unnecessary files!


Best Practice 6: Multi-line for Readability

Bad:

RUN apt-get update && apt-get install -y curl vim git wget htop

Good:

RUN apt-get update && apt-get install -y \
    curl \
    vim \
    git \
    wget \
    htop
# Easier to read and modify ✓

Practice Exercises

Let's practice creating Dockerfiles!

Exercise 1: Node.js Application

Create a simple Node.js app:

app.js:

const http = require('http');

const server = http.createServer((req, res) => {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end('<h1>Hello from Node.js in Docker!</h1>');
});

server.listen(3000, '0.0.0.0', () => {
    console.log('Server running on port 3000');
});

Your task: Create Dockerfile

Hints:

  • Use FROM node:18
  • WORKDIR /app
  • COPY app.js .
  • EXPOSE 3000
  • CMD ["node", "app.js"]

Build and run:

docker build -t node-app .
docker run -d -p 3000:3000 node-app

Test: http://localhost:3000


Exercise 2: Static Website with Nginx

Create index.html:

<!DOCTYPE html>
<html>
<head>
    <title>My Docker Site</title>
</head>
<body>
    <h1>Welcome to my Dockerized website!</h1>
    <p>This is served by Nginx running in a Docker container.</p>
</body>
</html>

Your task: Create Dockerfile

Hints:

  • Use FROM nginx:alpine
  • Copy index.html to /usr/share/nginx/html/
  • EXPOSE 80

Build and run:

docker build -t my-website .
docker run -d -p 8080:80 my-website

Test: http://localhost:8080


Summary

What We Learned:

✅ What a Dockerfile is
✅ Dockerfile instructions:
   ├── FROM (base image)
   ├── RUN (execute commands)
   ├── COPY (copy files)
   ├── WORKDIR (set directory)
   ├── CMD (default command)
   ├── ENTRYPOINT (fixed command)
   ├── ENV (environment variables)
   └── EXPOSE (document ports)
✅ Building images with docker build
✅ Layer caching
✅ Best practices
✅ .dockerignore
✅ Created real applications

No comments:

Post a Comment

PHASE 1 — Topic 4: How Socket.IO Works Internally (Engine.IO, Transports, and Fallback)

We now understand what Socket.IO is and why it is useful. In this post, we go one level deeper and look at what actually happens behind the ...