Building Images with Dockerfile

30 minLesson 4 of 5

Learning Objectives

  • Write Dockerfiles with FROM, RUN, COPY, CMD, and EXPOSE
  • Build and tag custom images
  • Understand image layers and caching
  • Push images to Docker Hub
  • Use .dockerignore for efficient builds

What is a Dockerfile?

A Dockerfile is a text file with instructions for building a Docker image. Each instruction creates a layer in the image.

Dockerfile Instructions

InstructionPurpose
FROMBase image to build upon
RUNExecute commands during build
COPYCopy files from host to image
ADDLike COPY, but supports URLs and tar extraction
WORKDIRSet working directory
EXPOSEDocument which ports the app uses
ENVSet environment variables
CMDDefault command when container starts
ENTRYPOINTFixed command (CMD becomes arguments)
VOLUMECreate mount point
LABELAdd metadata

Building a Python Flask App

Project Structure

my-app/
├── Dockerfile
├── requirements.txt
├── app.py
└── .dockerignore

app.py

from flask import Flask
 
app = Flask(__name__)
 
@app.route('/')
def index():
    return {'message': 'Hello from NextGen Playground!', 'status': 'running'}
 
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

requirements.txt

flask==3.0.0

Dockerfile

# Start from Python base image
FROM python:3.11-slim
 
# Set working directory
WORKDIR /app
 
# Copy requirements first (better caching)
COPY requirements.txt .
 
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
 
# Copy application code
COPY app.py .
 
# Document the port
EXPOSE 5000
 
# Command to run when container starts
CMD ["python", "app.py"]

.dockerignore

__pycache__
*.pyc
.git
.env
node_modules
.venv

Building the Image

# Build with tag
docker image build -t my-flask-app:1.0 .
 
# Build with different tag
docker image build -t my-flask-app:latest .
 
# List images
docker image ls

The . is the build context — the directory Docker sends to the daemon.

Running Your Image

docker container run -d --name flask-app \
  -p 5000:5000 \
  my-flask-app:1.0
 
# Test it
curl http://localhost:5000

Understanding Layers & Caching

Each instruction creates a layer. Docker caches layers for faster rebuilds:

FROM python:3.11-slim          # Layer 1 (cached)
WORKDIR /app                   # Layer 2 (cached)
COPY requirements.txt .        # Layer 3 (cached if file unchanged)
RUN pip install -r requirements.txt  # Layer 4 (cached if requirements unchanged)
COPY app.py .                  # Layer 5 (rebuilt if code changes)
CMD ["python", "app.py"]       # Layer 6

Best practice: Put things that change least at the top, things that change most at the bottom.

CMD vs ENTRYPOINT

# CMD: default command, can be overridden
CMD ["python", "app.py"]
# docker run my-app              → runs python app.py
# docker run my-app bash         → runs bash instead
 
# ENTRYPOINT: fixed command, CMD becomes arguments
ENTRYPOINT ["python"]
CMD ["app.py"]
# docker run my-app              → runs python app.py
# docker run my-app test.py      → runs python test.py

Multi-Stage Builds

Reduce final image size by separating build and runtime:

# Stage 1: Build
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
# Stage 2: Production (only runtime needed)
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Result: Final image contains only nginx + built files (not Node.js, npm, source code).

Pushing to Docker Hub

# Login
docker login
 
# Tag for Docker Hub (username/image:tag)
docker image tag my-flask-app:1.0 yourusername/my-flask-app:1.0
 
# Push
docker image push yourusername/my-flask-app:1.0
 
# Others can now pull it
docker image pull yourusername/my-flask-app:1.0

Saving/Loading Images (Offline)

# Save to tar file
docker image save -o my-app.tar my-flask-app:1.0
 
# Load from tar file (on another machine)
docker image load -i my-app.tar

Dockerfile Best Practices

  1. Use specific base image tagspython:3.11-slim not python:latest
  2. Minimize layers — Chain RUN commands with &&
  3. Order for caching — Dependencies before source code
  4. Use .dockerignore — Exclude unnecessary files
  5. Don't run as root — Add USER instruction
  6. Use multi-stage builds — Smaller production images
  7. One process per container — Keep containers focused

Summary

  • Dockerfiles define how to build images layer by layer
  • FROMRUNCOPYEXPOSECMD is the typical flow
  • Layer caching speeds up rebuilds — order instructions wisely
  • Multi-stage builds create small production images
  • Push to Docker Hub to share images with your team
  • Use .dockerignore to exclude unnecessary files from builds

Next Steps

Next, we'll use Docker Compose to define and run multi-container applications with a single command.