第 6 章 Dockerfile:构建自己的镜像
本章目标:掌握 Dockerfile 的核心指令,理解构建上下文与层缓存机制,分清 CMD 与 ENTRYPOINT,能把自己的应用打包成镜像。
1. 从应用到镜像:构建流程
镜像的"源代码"就是 Dockerfile——一份按行执行的构建说明书。构建命令:
bash
docker build -t demo-app . # -t 给镜像命名打标签;. 是"构建上下文"路径流程:docker CLI 把构建上下文(. 指当前目录,实际是"当前目录及其子目录")打包发给 daemon,daemon 逐行执行 Dockerfile 指令,每行指令生成一个只读层(呼应第 2 章的分层),最终产出镜像。
构建上下文有两个后果:
- Dockerfile 里的
COPY只能引用上下文内的文件——别把整个项目根目录当上下文,只给 Dockerfile 需要的; - 上下文会被整体发送给 daemon,用
.dockerignore排除无用文件(node_modules、.git、缓存等),加快构建:
text
# .dockerignore
node_modules
.git
*.log
__pycache__2. 核心指令
| 指令 | 作用 | 说明 |
|---|---|---|
FROM <镜像> | 指定基础镜像 | 必须是第一条指令;可用 AS 别名 命名阶段(第 7 章多阶段构建用) |
RUN <命令> | 构建期执行命令 | 每行生成一层;装依赖、编译都在这里 |
COPY <源> <目标> | 复制上下文中的文件进镜像 | 源必须在上下文内 |
ADD <源> <目标> | 增强版 COPY | 支持 URL、自动解压 tar;能用 COPY 就别用 ADD |
WORKDIR <路径> | 设置工作目录 | 影响 RUN/CMD/ENTRYPOINT/COPY 的相对路径,不存在会自动创建 |
ENV 键=值 | 设置环境变量 | 运行时也生效;构建期用 ARG(不写入镜像) |
ARG 名=默认值 | 构建参数 | 仅 docker build --build-arg 时传入,不进运行环境 |
EXPOSE <端口> | 声明容器监听端口 | 只是文档声明,不发布端口(发布还得 -p) |
USER <用户> | 指定运行用户 | 安全加固关键(第 10 章展开) |
CMD [...] / ENTRYPOINT [...] | 定义启动命令 | 区别见第 3 节 |
HEALTHCHECK | 定义健康检查 | 第 10 章展开 |
一个典型示例:
dockerfile
FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py .
ENV PORT=8000
EXPOSE 8000
USER nobody
CMD ["python", "app.py"]3. CMD vs ENTRYPOINT:全章唯一的易混点
两者都定义"容器启动时跑什么",区别在于能否被 docker run 覆盖:
| CMD | ENTRYPOINT | |
|---|---|---|
docker run <镜像> | 使用 CMD | 使用 ENTRYPOINT |
docker run <镜像> xxx | CMD 被 xxx 整体替换 | xxx 作为参数追加给 ENTRYPOINT |
| 典型用途 | 提供默认参数 | 固定主程序(框架) |
dockerfile
CMD ["python", "app.py"] # docker run img 时执行 python app.py
# docker run img python test.py → 执行 python test.py(CMD 被替换)
ENTRYPOINT ["python"] # 固定主程序
# docker run img app.py → 执行 python app.py(追加参数)最佳实践:ENTRYPOINT 定框架 + CMD 给默认参数:
dockerfile
ENTRYPOINT ["python"]
CMD ["app.py"] # 默认 python app.py;docker run img test.py 则 python test.py注意 JSON 数组形式(["python", "app.py"])不会经过 shell;字符串形式(CMD python app.py)会走 /bin/sh -c,能解析环境变量,但 PID 1 是 shell 而非应用进程(信号转发有差异,生产建议 JSON 形式)。
4. 构建缓存:顺序决定构建速度
层缓存规则:某条指令没变,则它的层及其之前的层都复用缓存。因此指令顺序要"把经常变的放后面":
dockerfile
# ❌ 依赖安装写在源码复制之后:每次改代码都要重新 pip install
COPY . .
RUN pip install -r requirements.txt
# ✅ 先复制依赖清单装依赖(依赖不常变,缓存命中),再复制源码
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .常用参数:
bash
docker build --no-cache -t demo-app . # 强制全部重构建(排查缓存问题时用)
docker build --target builder -t x . # 构建到指定阶段(多阶段构建用,见第 7 章)观察缓存命中的标志:构建日志中该层显示
CACHED。
5. 完整示例:把 Python 应用打成镜像
配套 examples/06-dockerfile/,零第三方依赖(用标准库 http.server),构建无需联网:
python
# examples/06-dockerfile/app.py
import http.server
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
body = b"Hello from Dockerfile demo!\n"
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
http.server.HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()dockerfile
# examples/06-dockerfile/Dockerfile
FROM python:3.12-alpine
WORKDIR /app
COPY app.py .
EXPOSE 8000
USER nobody
CMD ["python", "app.py"]构建、运行、验证、清理:
bash
cd examples/06-dockerfile
docker build -t demo-app .
docker run -d --name demo -p 8085:8000 demo-app
curl http://localhost:8085 # 输出 Hello from Dockerfile demo!
docker rm -f demo
docker rmi demo-app常见误区
- 误区:
EXPOSE之后外部就能访问容器。 正确:EXPOSE只是声明(给读者和-P用的文档),真正发布端口靠docker run -p。 - 误区:
ADD比COPY强,优先用 ADD。 正确:能COPY就COPY;ADD的自动解压/URL 行为容易带来意外。 - 误区:改代码后要重新
docker build才生效。 正确:开发期用 bind mount 挂源码(第 4 章)即可热更新;build 是交付时才做的事。 - 误区:容器里默认就该用 root。 正确:官方基础镜像默认 root,生产务必
USER切换到非特权用户(第 10 章)。 - 误区:
CMD和ENTRYPOINT随便选。 正确:docker run要能追加/覆盖参数时用 ENTRYPOINT + CMD 组合。
小结
docker build -t 名字 .:上下文是.,用.dockerignore控制发送内容。- 指令要点:
FROM开头;RUN构建期执行;COPY复制文件(优先于ADD);WORKDIR设置目录;EXPOSE只是声明;USER切非 root。 - CMD 可被
docker run覆盖,ENTRYPOINT 不可;组合使用:ENTRYPOINT 定框架 + CMD 给默认参数。 - 缓存规则:不常变的放前面、常变的放后面;
--no-cache强制重建。 - 完整构建示例见
examples/06-dockerfile/,docker build→docker run -p→curl验证。
练习
- 基于
examples/06-dockerfile/构建镜像并运行,用curl验证输出;再用docker history demo-app观察构建出的层。 - 给 Dockerfile 增加
RUN apk add curl(或一个ENV),第二次构建观察哪些层显示CACHED。 - 把
CMD ["python", "app.py"]改成ENTRYPOINT ["python"]+CMD ["app.py"],验证docker run demo-app与docker run demo-app -c "print(1)"的行为差异。 - 写一个
.dockerignore排除某个大文件后重新构建,对比构建输出与镜像大小。
可运行示例见
examples/06-dockerfile/,按其中的 README 步骤执行。