Dockerfile Basics
A Dockerfile is the "source code" of an image—by writing a Dockerfile, you can build a reproducible application image with a single command.
1. What You'll Learn
- Basic Syntax and Structure of a Dockerfile
- Policy for selecting the base image using the "FROM" clause
- RUN: Best Practices for Executing Build Commands
- The Difference Between CMD and ENTRYPOINT
- Setting Up the Context and .dockerignore
2. A True Story of a Python Developer
(1) Pain Point: Having to manually set up the environment with every deployment
Alice wrote a Python web application. Every time she deploys it, she has to manually install Python, set up a virtual environment, install dependencies, and copy the code. She has to repeat this process three times—once for the test server, once for the pre-production environment, and once for the production server—which amounts to a total of three repetitions, each with slight variations.
(2) Solutions for Dockerfile Automation
Bob said, "Write a Dockerfile, 'pack' your application into an image, and then you'll be able to run it anywhere with a single command."
# A simple Dockerfile for a Flask application
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
(3) Benefits: Build Once, Run Anywhere
After Alice built the image using docker build -t myapp:1.0 ., all three environments ran on the same image, reducing deployment time from 30 minutes to 3 minutes and eliminating environment inconsistencies.
3. Basic Structure of a Dockerfile
A Dockerfile is a plain-text file that contains a sequence of instructions for building an image. Each instruction builds an image layer.
graph LR
DF["Dockerfile<br/>Sequence of Instructions"] -->|docker build| IMG["Image<br/>Read-Only Overlay"]
IMG -->|docker run| CTN["Container<br/>Writable layer + Read-Only Layer"]
(1) Timing of Instruction Execution
| Timing | Command | Description |
|---|---|---|
| During build | FROM / RUN / COPY / ADD / ARG |
Generate image layer |
| Runtime | CMD / ENTRYPOINT / ENV / EXPOSE / USER |
Defines container behavior |
(2) Basic Dockerfile Template
# 1. Base image
FROM python:3.12-slim
# 2. Set working directory
WORKDIR /app
# 3. Copy dependency file first (cache optimization)
COPY requirements.txt .
# 4. Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# 5. Copy application code
COPY . .
# 6. Define the default command
CMD ["python", "app.py"]
4. FROM: Select a base image
FROM is the first instruction in a Dockerfile; it specifies the base image for the build.
(1) Base Image Selection Strategy
| Strategy | Image | Size | Use Case |
|---|---|---|---|
| Official Language Mirror | python:3.12-slim |
155 MB | Python Project |
| Alpine variant | python:3.12-alpine |
50 MB | Disk space extremely low |
| Multi-stage build | golang:1.22 → alpine |
12 MB | Go/Rust compiled languages |
| Minimalist OS | debian:bookworm-slim |
74 MB | Requires a custom environment |
▶ Example: The Simplest Dockerfile (Difficulty: ⭐)
# Minimal Dockerfile: just prints hello
FROM alpine:3.19
CMD ["echo", "Hello from Docker!"]
# Build and run
docker build -t hello:1.0 .
docker run --rm hello:1.0
Hello from Docker!
5. RUN: Execute the build command
RUN Executes a command during the build, and writes the result to a new image layer.
(1) Two Formats
| Format | Syntax | Features |
|---|---|---|
| Shell format | RUN apt-get install nginx |
Executes /bin/sh -c by default; supports pipes |
| Exec Mode | RUN ["apt-get", "install", "nginx"] |
Execute directly without launching a shell |
▶ Example: RUN to install dependencies (Difficulty: ⭐⭐)
# Best practice: combine RUN commands to reduce layers
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
nginx && \
rm -rf /var/lib/apt/lists/*
apt-get commands into a single RUN to reduce the number of image layers. rm -rf /var/lib/apt/lists/* clears the APT cache to reduce the image size.
(2) Principles of RUN Chain Merging
| Method | Result | Notes |
|---|---|---|
Multiple RUN |
Each entry creates a layer | Many layers, large file size |
Merge RUN && |
One layer per track | Few layers, compact size |
| Clear Cache | rm -rf apt/lists |
Clears cache within the same level |
| Clean separately | Next RUN rm |
Invalid—the previous layer has cured |
# Bad: creates 2 layers, apt cache is baked into layer 1
RUN apt-get update
RUN apt-get install -y nginx
# Good: creates 1 layer, cache cleaned in the same layer
RUN apt-get update && \
apt-get install -y nginx && \
rm -rf /var/lib/apt/lists/*
6. CMD and ENTRYPOINT
Both CMD and ENTRYPOINT define the commands to be executed when the container starts, but they behave differently.
(1) Comparison of Three Startup Commands
| Dimension | CMD |
ENTRYPOINT |
|---|---|---|
| Purpose | Provide default commands | Define a fixed entry point |
| Can be overridden | docker run Parameter overrides directly |
Requires --entrypoint to override |
| Combination | Can be used with ENTRYPOINT | Can be used with CMD command-line arguments |
| Multiple | Only the last one takes effect | Only the last one takes effect |
(2) The Three Formats of CMD
| Format | Syntax | Recommendation Level | Description |
|---|---|---|---|
| Exec Format | CMD ["python", "app.py"] |
⭐⭐⭐ | Executes directly; signals are passed correctly |
| Shell format | CMD python app.py |
⭐ | As a child process of /bin/sh -c, SIGTERM is not passed |
| Parameter Format | CMD ["--port", "8080"] |
⭐⭐ | Use with ENTRYPOINT |
▶ Example: The Difference Between CMD and ENTRYPOINT (Difficulty: ⭐⭐)
# Dockerfile with CMD: command can be easily overridden
FROM alpine:3.19
CMD ["echo", "Hello default"]
# Default: runs CMD
docker run --rm test-cmd
# Output: Hello default
# Override CMD with custom command
docker run --rm test-cmd echo "Custom message"
# Output: Custom message
# Dockerfile with ENTRYPOINT: command stays, args are appended
FROM alpine:3.19
ENTRYPOINT ["echo"]
CMD ["Hello default"]
# Default: runs ENTRYPOINT + CMD
docker run --rm test-entry
# Output: Hello default
# Append arguments (don't override ENTRYPOINT)
docker run --rm test-entry "Custom message"
# Output: Custom message
# Override ENTRYPOINT (rarely needed)
docker run --rm --entrypoint sh test-entry -c "ls /"
▶ Example: ENTRYPOINT + CMD Combination (Difficulty: ⭐⭐⭐)
This is the best practice approach—ENTRYPOINT specifies the program to run, and CMD provides the default arguments:
# Entrypoint + CMD pattern
FROM python:3.12-slim
WORKDIR /app
COPY app.py .
ENTRYPOINT ["python", "app.py"]
CMD ["--host", "0.0.0.0", "--port", "5000"]
# Default: uses CMD arguments
docker run --rm myapp
# Equivalent to: python app.py --host 0.0.0.0 --port 5000
# Override just the arguments
docker run --rm myapp --port 8080
# Equivalent to: python app.py --port 8080
7. Setting Up the Context and .dockerignore
(1) Setting the Context
The last parameter of docker build, ., does not refer to the "current directory," but rather to the build context—the Docker Client sends all files in this directory to the Daemon.
graph LR
CTX["Build Context<br/>(. Table of Contents)"] -->|Send File| D["Docker Daemon"]
D -->|Dockerfile in COPY/ADD| IMG["Image Layers"]
node_modules in the directory, 1 GB will be sent to the daemon during the build (even if the Dockerfile does not COPY it). .dockerignore can exclude unnecessary files.
▶ Example: How .dockerignore Works (Difficulty: ⭐⭐)
# .dockerignore - exclude files from build context
node_modules
.git
__pycache__
*.pyc
.env
Dockerfile
docker-compose*.yml
README.md
.vscode
(2) Why should the COPY directive for dependency files come before the COPY directive for source code?
# Good: dependency change rate < code change rate
# When code changes, dependency layer uses cache
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
# Bad: any file change invalidates the pip install layer
COPY . .
RUN pip install -r requirements.txt
| Change Scenario | Good Version | Bad Version |
|---|---|---|
| Modify code only | ✅ Use pip cache (seconds) | ❌ Rebuild pip (minutes) |
| Change Dependencies | ✅ Rebuild pip layer (required) | ❌ Rebuild pip layer (as above) |
8. Common Parameters for docker build
| Parameter | Function | Example |
|---|---|---|
-t |
Image Name: Tag | -t myapp:1.0 |
-f |
Specify Dockerfile path | -f Dockerfile.prod . |
--build-arg |
Pass build parameters | --build-arg VERSION=2.0 |
--no-cache |
Do not use cache | --no-cache |
--target |
Build to a specified stage | --target builder |
--platform |
Specify Target Platform | --platform linux/arm64 |
▶ Example: Build an image and view the layer history (Difficulty: ⭐⭐)
# Build with tag
docker build -t myapp:1.0 .
# View image layers
docker history myapp:1.0
9. Complete Example: Writing a Dockerfile for a Flask Application
# ============================================
# Dockerfile for a Flask web application
# Demonstrates: FROM, WORKDIR, COPY, RUN, CMD
# ============================================
# Use official Python slim image
FROM python:3.12-slim
# Set working directory inside container
WORKDIR /app
# Copy dependency file first (cache optimization)
COPY requirements.txt .
# Install dependencies (clean cache in same layer)
RUN pip install --no-cache-dir -r requirements.txt
# Copy application source code
COPY . .
# Expose the application port (documentation only)
EXPOSE 5000
# Run the Flask application
CMD ["python", "app.py"]
# Build the image
docker build -t flask-app:1.0 .
# Run the container
docker run -d -p 5000:5000 --name my-flask flask-app:1.0
# Test the application
curl http://localhost:5000
# View image size and layers
docker images flask-app
docker history flask-app:1.0
# docker images flask-app
REPOSITORY TAG IMAGE ID SIZE
flask-app 1.0 a1b2c3d4e5f6 180MB
# docker history flask-app:1.0
IMAGE CREATED CREATED BY SIZE
a1b2c3d4e5f6 5 seconds ago CMD ["python" "app.py"] 0B
<missing> 5 seconds ago COPY . . 2.5kB
<missing> 5 seconds ago RUN pip install --no-cache-dir... 45MB
<missing> 5 seconds ago COPY requirements.txt . 58B
<missing> 5 seconds ago WORKDIR /app 0B
❓ FAQ
python app.py), while CMD provides default arguments (such as --port 5000). Arguments passed to docker run override CMD but not ENTRYPOINT, enabling a "fixed executable + flexible arguments" setup.. at the end of the docker build command specifies the build context directory. The Docker client packages all files in this directory and sends them to the daemon. The COPY and ADD commands in the Dockerfile can only reference files within the build context. Use .dockerignore to exclude unnecessary files, which speeds up the build and reduces the size of the build context.COPY exists; ③ Interactively debug—docker run -it <last-successful-layer> bash enter the last successful image layer to manually troubleshoot.\ to continue the line and && to chain commands. This is the standard way to write a Dockerfile, which reduces the number of layers while maintaining readability. For example: RUN apt-get update && \ + apt-get install -y nginx && \ + rm -rf /var/lib/apt/lists/*.📖 Summary
- A Dockerfile is the "source code" of an image; each instruction generates a read-only layer
- FROM: Choose a base image: "slim" offers good compatibility, while "alpine" is minimal but may have compatibility issues
- Use the RUN merge command to reduce the number of layers, and clear the cache within each layer to reduce the file size
- CMD provides a default command (which can be overridden), while ENTRYPOINT defines a fixed entry point (which is difficult to override)
- The ENTRYPOINT + CMD combination is a best practice: a fixed program + flexible parameters
- COPY dependency files before the source code, and use layer caching to speed up the build
📝 Exercises
- Basic Problem (Difficulty ⭐): Write a Dockerfile for a Node.js application, using
node:20-alpineas the base image,npm installto install dependencies, andnode server.jsto start the application. - Advanced Problem (Difficulty ⭐⭐): Build an image and run a container. Use
docker historyto analyze the size of each layer in the image, identify the largest layer, and explain why. - Challenge (Difficulty: ⭐⭐⭐): Create a .dockerignore file to exclude node_modules and .git, and compare the difference in build context size with and without the .dockerignore file (Hint: Look for the line "Sending build context to Docker daemon" in the
docker buildoutput).



