Skip to content

第 6 章 Dockerfile:构建自己的镜像 ​

本章目标:掌握 Dockerfile 的核心指令,理解构建上下文与层缓存机制,分清 CMD 与 ENTRYPOINT,能把自己的应用打包成镜像。

1. 从应用到镜像:构建流程 ​

镜像的"源代码"就是 Dockerfile——一份按行执行的构建说明书。构建命令:

bash
docker build -t demo-app .    # -t 给镜像命名打标签;. 是"构建上下文"路径

流程:docker CLI 把构建上下文(. 指当前目录,实际是"当前目录及其子目录")打包发给 daemon,daemon 逐行执行 Dockerfile 指令,每行指令生成一个只读层(呼应第 2 章的分层),最终产出镜像。

构建上下文有两个后果:

  1. Dockerfile 里的 COPY 只能引用上下文内的文件——别把整个项目根目录当上下文,只给 Dockerfile 需要的;
  2. 上下文会被整体发送给 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 覆盖:

CMDENTRYPOINT
docker run <镜像>使用 CMD使用 ENTRYPOINT
docker run <镜像> xxxCMD 被 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 验证。

练习 ​

  1. 基于 examples/06-dockerfile/ 构建镜像并运行,用 curl 验证输出;再用 docker history demo-app 观察构建出的层。
  2. 给 Dockerfile 增加 RUN apk add curl(或一个 ENV),第二次构建观察哪些层显示 CACHED。
  3. 把 CMD ["python", "app.py"] 改成 ENTRYPOINT ["python"] + CMD ["app.py"],验证 docker run demo-app 与 docker run demo-app -c "print(1)" 的行为差异。
  4. 写一个 .dockerignore 排除某个大文件后重新构建,对比构建输出与镜像大小。

可运行示例见 examples/06-dockerfile/,按其中的 README 步骤执行。

📖本文阅读--次|📊全站访问--次|👥访客--人