第三章

Dockerfile:把应用装进集装箱

第二层 · 镜像构建/分层 把直播带货模块打包成镜像

一、从"装环境"到"写配方":Dockerfile 是什么

"小陈,还记得以前部署一个服务要装多少环境吗?Python、依赖、Nginx、配置……每一台机器都要手动来一遍,而且每一台装的都可能不一样。"老周说,"现在,我们把'装环境的所有步骤'写成一个文件——Dockerfile,它就是镜像的'配方'。"

Dockerfile:
  一个文本文件,用"指令"描述"怎么构建出一个镜像"
  = 把"装环境"的步骤代码化、标准化、可复用

  类比:
    手工部署 = 每次手动装环境(又慢又容易不一致)
    Dockerfile = 写一份"自动装环境脚本"(写一次,处处用)

"Dockerfile 的价值:'环境即代码'(Infrastructure as Code 的一种)——环境不再是玄学,是一个可以版本管理、审查、复用的文件。"


二、写第一个 Dockerfile:打包云间书店的 AI 荐书服务

老周带着小陈,给 AI 荐书服务写 Dockerfile:

# 1. 选一个基础镜像(爸爸是谁)
FROM python:3.11-slim

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

# 3. 先复制依赖清单,装依赖(分层优化,后讲)
COPY requirements.txt .
RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

# 4. 复制代码
COPY app/ ./app/

# 5. 声明端口(只是"声明",真正映射在 run 时做)
EXPOSE 8000

# 6. 容器启动时执行的命令
CMD ["python", "app/main.py"]

"六个核心指令,一个一个讲:"

FROM     :基础镜像("爸爸")。所有镜像都从某个基础镜像开始
WORKDIR  :设置工作目录(后续命令的默认目录)
COPY     :把本地文件复制进镜像
RUN      :构建时执行的命令(装依赖、编译……)
EXPOSE   :声明容器会监听哪些端口(文档作用)
CMD      :容器启动时运行的命令(只有一个 CMD 生效)

"注意区分 RUN 和 CMD:RUN 是'盖房子时干的活'(构建阶段执行一次),CMD 是'住进去后天天干的活'(每次启动都执行)。"


三、构建镜像:docker build

# 在 Dockerfile 所在目录执行
docker build -t yunjian/ai-recommend:v1 .

# 参数:
#   -t      :给镜像打标签(名字:版本)
#   .       :构建上下文(把当前目录发给 Docker)
#   --no-cache :强制重新构建(不用缓存,排障用)

# 查看构建出的镜像
docker images
# yunjian/ai-recommend   v1   ...

# 跑起来
docker run -d -p 8000:8000 --name ai01 yunjian/ai-recommend:v1

"docker build 会按 Dockerfile 的指令从上到下执行,每一条指令产生一个'镜像层'。 构建完成后,你就有了自己的第一个业务镜像——从'拉别人的镜像'到'造自己的镜像',这是质变。"


四、镜像分层:为什么 Docker 快又省

"构建的时候你有没有注意到输出里每个指令前面有个 ---> ?那是镜像层(Layer)。"老周画:

镜像分层(Layer):
  FROM python:3.11-slim      → 层1(基础层)
  WORKDIR /app                → 层2(很小)
  COPY requirements.txt .     → 层3
  RUN pip install ...         → 层4(最大)
  COPY app/ ./app/            → 层5
  CMD [...]                   → 层6(元数据)

  每一层都是"只读层",叠加在一起 = 完整镜像
  容器运行时会加一个"可写层"(容器内的修改都在这里)

分层的威力:
  ① 缓存:改第 5 层,前 4 层缓存直接复用(秒级重建!)
     → 所以把"不容易变的"(依赖)放前面,"容易变的"(代码)放后面
  ② 共享:两个镜像共用基础层 → 磁盘只存一份
  ③ 传输:拉镜像只下载"没有的层"(增量拉取,快)

坑:
  ❌ 把 COPY 代码写在 pip install 前面 → 每次改代码都重装依赖
  ✅ 依赖先复制、代码后复制 → 改代码只重建最后一两层

"写 Dockerfile 的第一个优化原则:把'变化慢的'放前面,'变化快的'放后面。 这样开发迭代时构建飞快。"


五、多阶段构建:让镜像"瘦身"

小陈构建完镜像,一看大小:"师傅,900MB!这也太大了吧?"

"因为基础镜像里带了编译器、一堆工具,但运行时根本用不到。"老周说,"用多阶段构建(Multi-stage Build)给镜像瘦身:"

# 阶段1:构建环境(带全套工具)
FROM golang:1.21 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o app .

# 阶段2:运行环境(只要运行文件,越小越好)
FROM alpine:latest
WORKDIR /app
COPY --from=builder /src/app ./app   # 只复制编译产物
EXPOSE 8080
CMD ["./app"]

# 结果:从 900MB → 20MB !

"多阶段构建 = 第一阶段'生产',第二阶段'精简打包'——只把运行需要的东西带到最终镜像。 生产环境镜像越小越好:下载快、启动快、攻击面小。"

镜像瘦身三板斧:
  ① 多阶段构建(build 阶段和 run 阶段分离)
  ② 选轻量基础镜像(alpine / slim 版本)
  ③ 清理垃圾(不装不需要的包,删缓存/临时文件)

六、章末:老周的第二层总结

第二层:镜像 Image
├── Dockerfile:镜像的"配方"(环境即代码)
│   FROM/WORKDIR/COPY/RUN/EXPOSE/CMD
│   RUN=构建时干,CMD=启动时干
├── docker build:按配方构建镜像(-t 打标签)
├── 镜像分层:只读层叠加 + 容器可写层
│   缓存复用(依赖放前代码放后)/ 层共享 / 增量传输
└── 多阶段构建:构建环境和运行环境分离,镜像瘦身

"小陈,你现在能'造镜像'了。但容器跑起来之后,一堆问题等着你:容器挂了怎么重启?想进容器看看日志怎么看?容器删了,里面的数据还在吗?——下一章开始,我们进入容器的'一生':生命周期、日志、还有那个最经典的坑——容器里写数据,容器一删,全没了。"

✌ 语言