一、从"装环境"到"写配方":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 打标签)
├── 镜像分层:只读层叠加 + 容器可写层
│ 缓存复用(依赖放前代码放后)/ 层共享 / 增量传输
└── 多阶段构建:构建环境和运行环境分离,镜像瘦身
"小陈,你现在能'造镜像'了。但容器跑起来之后,一堆问题等着你:容器挂了怎么重启?想进容器看看日志怎么看?容器删了,里面的数据还在吗?——下一章开始,我们进入容器的'一生':生命周期、日志、还有那个最经典的坑——容器里写数据,容器一删,全没了。"