一、学习目标

  1. 理解 Dockerfile 是什么以及它的作用

  2. 掌握 Dockerfile 核心指令:FROM、WORKDIR、COPY、RUN、CMD

  3. 编写第一个 Dockerfile(基于 Python Flask 应用)

  4. 使用 docker build 构建镜像

二、什么是 Dockerfile?

Dockerfile 是一个纯文本文件,里面包含了一系列构建镜像的指令。Docker 可以读取这些指令并自动构建出一个镜像。

Dockerfile 的作用

  • 自动化构建:将应用及其运行环境固化为代码(Infrastructure as Code)

  • 可重复性:任何人只要拿到 Dockerfile,就能构建出完全相同的镜像

  • 版本控制:Dockerfile 可以像源代码一样提交到 Git 仓库

一个简单的例子

# 使用 Python 官方镜像作为基础
FROM python:3.9-slim

# 设置工作目录
WORKDIR /app

# 复制当前目录下的所有文件到容器内的 /app
COPY . /app

# 安装 Python 依赖
RUN pip install -r requirements.txt

# 声明容器启动时运行的命令
CMD ["python", "app.py"]

三、核心指令详解

1. FROM —— 指定基础镜像

作用:每个 Dockerfile 的第一条指令必须是 FROM,它指定了构建所使用的“地基”镜像。

# 语法
FROM <镜像名>[:<标签>]

# 示例
FROM ubuntu:22.04        # 基于 Ubuntu 22.04
FROM python:3.9-slim     # 基于精简版 Python 3.9
FROM alpine:latest       # 基于 Alpine Linux(极小体积)
FROM scratch             # 从零开始(用于静态二进制文件)

最佳实践:尽量选择官方镜像,并指定具体标签(如 python:3.9-slim),不要使用 latest(不可复现)。

2. WORKDIR —— 设置工作目录

作用:为后续的 RUN、CMD、COPY 等指令设置当前工作目录。如果目录不存在,会自动创建。

# 语法
WORKDIR <路径>

# 示例
WORKDIR /app                     # 绝对路径
WORKDIR /usr/local/src
WORKDIR app                      # 相对路径(相对于上一个 WORKDIR)

# 可以多次使用,支持相对路径
WORKDIR /home
WORKDIR myapp      # 最终路径 = /home/myapp

最佳实践:使用绝对路径,避免混淆。先 WORKDIR 再 COPY,可以简化复制路径。

3. COPY —— 复制文件

作用:将构建上下文(通常是 Dockerfile 所在目录)中的文件/目录复制到镜像中。

# 语法
COPY <源路径>... <目标路径>

# 示例
COPY . /app                 # 将当前目录所有文件复制到 /app
COPY requirements.txt /app/requirements.txt
COPY --chown=www-data:www-data index.html /var/www/
COPY --from=builder /output /app   # 多阶段构建中从上一阶段复制

# 通配符
COPY hom* /mydir/
COPY abc?.txt /mydir/

注意:

  • 源路径必须在构建上下文中(即不能 COPY ../xxx)

  • 目标路径可以是绝对路径,也可以是相对 WORKDIR 的相对路径

  • 如果目标路径不存在,Docker 会自动创建

4. RUN —— 执行命令

作用:在构建过程中执行命令,通常用于安装软件、配置环境。每个 RUN 都会在镜像中创建一个新层。

# 两种语法形式
# shell 形式(默认使用 /bin/sh -c)
RUN apt-get update && apt-get install -y curl

# exec 形式(推荐,不会启动 shell 解析)
RUN ["apt-get", "update"]

# 示例
RUN pip install flask redis
RUN npm install --production
RUN mkdir -p /app/data && chown 1000:1000 /app/data

# 多行命令(使用 \ 换行,提高可读性)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        build-essential \
        curl \
        git && \
    rm -rf /var/lib/apt/lists/*

最佳实践:

  • 将多个 RUN 合并为一个(减少层数,缩小镜像体积)

  • 安装完成后清理临时文件(如 apt-get clean)

  • 使用 && 确保命令失败时构建失败

5. CMD —— 容器启动默认命令

作用:指定容器启动时执行的默认命令。每个 Dockerfile 只能有一个 CMD,最后一个生效。CMD 可以被 docker run 后面的命令覆盖。

# 三种形式
# 1. exec 形式(推荐)
CMD ["python", "app.py"]

# 2. 作为 ENTRYPOINT 的默认参数
CMD ["--port", "8080"]

# 3. shell 形式
CMD python app.py

# 示例
CMD ["nginx", "-g", "daemon off;"]
CMD ["flask", "run", "--host=0.0.0.0"]

与 RUN 的区别:

  • RUN 在 构建时 执行,结果保存到镜像中

  • CMD 在 运行时 执行,只是默认命令,可以被覆盖

补充指令(了解即可,后面会深入学习)

指令作用
ADD类似 COPY,但支持 URL 下载和自动解压 tar(不推荐优先使用)
ENV设置环境变量,构建和运行时都存在
ARG构建参数,只在构建时有效
EXPOSE声明容器监听端口(只是文档,不会真正暴露)
ENTRYPOINT与 CMD 类似,但不会被覆盖,常与 CMD 配合使用

四、实战:为一个 Flask 应用编写 Dockerfile

1. 准备 Flask 应用

首先创建一个新目录,并在其中创建以下文件:

目录结构:

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

app.py(简单的 Flask Web 应用):

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello from Docker! I am running inside a container.'

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

requirements.txt:

flask==2.3.2

2. 编写 Dockerfile

在同一目录下创建 Dockerfile(无扩展名):

# 1. 指定基础镜像
FROM python:3.9-slim

# 2. 设置工作目录
WORKDIR /app

# 3. 复制依赖文件(先复制 requirements.txt,利用缓存)
COPY requirements.txt .

# 4. 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt

# 5. 复制应用源代码
COPY app.py .

# 6. 声明容器运行时监听的端口(仅文档作用)
EXPOSE 5000

# 7. 定义容器启动时运行的命令
CMD ["python", "app.py"]

逐行解释:

  • FROM python:3.9-slim:使用官方精简版 Python 镜像,体积小,适合生产。

  • WORKDIR /app:所有后续命令都在 /app 目录下执行。

  • COPY requirements.txt .:只复制依赖文件,这样在代码修改时,只要 requirements.txt 没变,Docker 就会使用缓存的依赖安装层,加速构建。

  • RUN pip install --no-cache-dir -r requirements.txt:安装依赖,--no-cache-dir 可以减小镜像体积。

  • COPY app.py .:复制应用代码。

  • EXPOSE 5000:告诉使用者这个容器会监听 5000 端口。

  • CMD ["python", "app.py"]:启动 Flask 应用。

3. 构建镜像

在 my-flask-app 目录下执行:

docker build -t my-first-flask-app .

参数说明:

  • -t my-first-flask-app:给镜像起一个名字(tag)

  • . :构建上下文路径(当前目录),Docker 会把这个目录下的所有文件发送给守护进程

构建过程输出示例:

[+] Building 15.2s (10/10) FINISHED
 => [1/5] FROM python:3.9-slim
 => [2/5] WORKDIR /app
 => [3/5] COPY requirements.txt .
 => [4/5] RUN pip install --no-cache-dir -r requirements.txt
 => [5/5] COPY app.py .
 => exporting to image
 => => naming to docker.io/library/my-first-flask-app

4. 运行镜像并测试

# 运行容器,映射宿主机 5000 端口到容器 5000 端口
docker run -d --name my-flask-container -p 5000:5000 my-first-flask-app

# 检查容器是否运行
docker ps

# 访问应用(使用 curl 或浏览器)
curl http://localhost:5000
# 应该输出:Hello from Docker! I am running inside a container.

5. 查看日志和停止容器

# 查看容器日志
docker logs my-flask-container

# 停止并删除容器
docker stop my-flask-container
docker rm my-flask-container

五、Dockerfile 编写最佳实践

1. 利用构建缓存

将不经常变化的指令放在前面,经常变化的代码放在后面。

# ✅ 好:依赖放前面,代码放后面
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

# ❌ 不好:复制所有代码在前,导致每次代码修改都要重装依赖
COPY . .
RUN pip install -r requirements.txt

2. 合并 RUN 命令减少层数

# ❌ 不好:多个 RUN,产生多个层
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# ✅ 好:合并为一个 RUN
RUN apt-get update && \
    apt-get install -y curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

3. 使用 .dockerignore 文件

创建 .dockerignore 文件,排除不需要的文件(如 __pycache__、.git、venv),加快构建速度并减小镜像体积。

# .dockerignore 内容
__pycache__
*.pyc
.git
.venv
.env
README.md

4. 选择合适的基础镜像

  • 生产环境优先选择 -alpine 或 -slim 版本,体积小

  • 开发环境可以使用完整镜像(如 python:3.9)

5. 固定版本号

避免使用 latest,指定具体版本(如 python:3.9-slim),确保环境一致性。

6. 以非 root 用户运行(安全性)

默认容器内是 root 用户,最佳实践是创建普通用户。

FROM python:3.9-slim

RUN useradd -m -u 1000 appuser
WORKDIR /app
COPY . .
RUN chown -R appuser:appuser /app

USER appuser

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

六、常用构建命令

# 基础构建
docker build -t 镜像名:标签 .

# 不使用缓存(强制重新构建所有层)
docker build --no-cache -t my-image .

# 指定 Dockerfile 名称(默认是 'Dockerfile')
docker build -f Dockerfile.prod -t my-image .

# 构建时传递构建参数(ARG)
docker build --build-arg VERSION=1.2.3 -t my-image .

# 查看镜像构建历史
docker history my-image

# 查看镜像详情
docker inspect my-image

七、完整示例:多阶段构建(进阶)

对于一个需要编译的应用(如 Go、Java),可以使用多阶段构建来减小最终镜像体积。

# 第一阶段:构建(使用完整编译环境)
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp main.go

# 第二阶段:运行(使用极简基础镜像)
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]

八、常见问题排查

Q1:构建时提示 COPY failed: file not found

原因:源文件路径错误或文件不存在。
解决:检查源文件是否在构建上下文内,路径是否正确。

Q2:容器启动后立即退出

原因:CMD 指定的命令不是持续运行的(比如直接运行 bash)。
解决:对于前台应用(如 Flask),确保 CMD 是阻塞命令;对于调试,可以先运行 sleep infinity。

Q3:镜像体积过大

解决:

  • 使用 Alpine 或 slim 基础镜像

  • 合并 RUN 指令并清理临时文件

  • 使用多阶段构建

  • 检查是否有不必要的文件(使用 .dockerignore)

Q4:pip install 速度慢

解决:使用国内镜像源(在 Dockerfile 中配置):

RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

九、课后练习

练习 1:构建一个静态网站镜像

# 使用 nginx:alpine 作为基础镜像
# 创建 index.html 文件,内容任意
# 复制到 /usr/share/nginx/html
# 构建并运行,通过浏览器访问

解析:

FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

练习 2:修改 Flask 应用

  1. 修改 app.py,增加一个新路由 /health,返回 OK

  2. 重新构建镜像(无需修改 Dockerfile)

  3. 重启容器,测试 /health 端点

练习 3:添加环境变量

修改 Dockerfile,使用 ENV 指令设置 FLASK_ENV=production,并在 app.py 中打印该环境变量。

十、小结

今天你学会了:

  • Dockerfile 的核心指令及其含义

  • 如何为一个 Flask 应用编写 Dockerfile

  • 使用 docker build 构建镜像

  • 运行自己的第一个定制镜像

今日检查清单:

  • 理解 Dockerfile 的作用

  • 能独立写出一个包含 FROM、WORKDIR、COPY、RUN、CMD 的 Dockerfile

  • 成功构建镜像并运行

  • 能通过浏览器或 curl 访问到 Flask 应用的输出

恭喜你完成了第五天的学习!明天我们将深入数据持久化(Volume),解决容器删除后数据丢失的问题。

更多推荐