第4章 容器化基础回顾:Dockerfile最佳实践、镜像分层原理、多阶段构建、镜像仓库管理
好,咱们进入第四章。说实话,很多同学觉得Docker嘛,写个Dockerfile就完事了。但我在PaaS平台摸爬滚打这些年,发现容器化这块坑特别多。镜像动不动几个G,构建慢得要死,安全漏洞一堆——这些我都经历过。今天咱们就把这些痛点一个一个解决掉。
4.1 Dockerfile最佳实践:从能用变成好用
先说说Dockerfile怎么写才算好。我见过太多人把Dockerfile写成了一锅粥——所有命令堆在一起,镜像体积大得吓人。嗯,这里要注意几个核心原则。
核心原则:每个RUN指令都会产生一个新层。层越多,镜像越大,构建越慢。
我个人习惯的做法是:
- 合并RUN指令——能用
&&连起来的就别分开写 - 清理临时文件——apt安装完记得删缓存
- 固定基础镜像版本——别用
latest,否则哪天构建就炸了 - 使用.dockerignore——别把node_modules塞进构建上下文
举个例子,我之前接手一个项目,Dockerfile长这样:
FROM node:14
COPY . /app
WORKDIR /app
RUN npm install
RUN npm run build
CMD ["npm", "start"]
你看,这就有三个问题:第一,基础镜像没固定版本;第二,COPY了整个目录,包括node_modules;第三,两个RUN指令分开了。改完之后是这样的:
FROM node:14.17.0-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production && npm cache clean --force
COPY . .
RUN npm run build
CMD ["npm", "start"]
镜像体积从1.2G降到了280M。你想想看,这差距有多大。
4.2 镜像分层原理:为什么层数越少越好
Docker镜像的分层机制,说白了就是UnionFS的功劳。每一层都是只读的,只有最上面那层是可写的。我刚开始学的时候也纳闷——为什么改个文件就要重新构建整个层?
原因其实很简单:Docker的层是增量存储的。你改了Dockerfile里的一行,从那一行开始往后的所有层都得重建。这就是为什么我建议把不常变的部分放在前面——比如系统依赖、环境变量这些。
小技巧:把 apt-get update && apt-get install 放在Dockerfile靠前的位置。这样你改代码的时候,系统依赖层就不用重新构建了,能省不少时间。
我记得有一次,团队里有人把 COPY . . 放在了安装依赖之前。结果每次改代码,npm install都要重新跑一遍。构建一次要15分钟,整个团队都在等。后来我把顺序调了一下,构建时间直接降到了2分钟。
4.3 多阶段构建:瘦身利器
多阶段构建是我在PaaS平台上用得最多的技巧之一。说白了就是——构建环境和运行环境分开。你想想看,Go程序编译需要gcc、glibc这些,但运行的时候只需要一个二进制文件就够了。
我以前做过一个Java项目,用Maven构建,镜像里装了JDK、Maven、各种依赖库,最后镜像1.8G。用多阶段构建之后:
# 第一阶段:构建
FROM maven:3.8.4-openjdk-11 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# 第二阶段:运行
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
最终镜像只有180M。你想想看,从1.8G到180M,减少了90%的体积。这在PaaS平台上意味着什么?更快的部署、更少的存储、更低的网络开销。
注意:多阶段构建虽然好,但别滥用。如果你只需要一个阶段,强行拆成两个反而增加复杂度。我曾经见过有人把简单的Python应用拆成三个阶段,结果构建时间翻了一倍。
4.4 镜像仓库管理:别让你的镜像变成垃圾堆
镜像仓库管理这块,我踩过的坑最多。刚开始做PaaS平台的时候,我们用的私有仓库,结果半年之后镜像数量超过5000个,磁盘直接爆了。
这里我总结了几条经验:
- 命名规范要统一——比如
项目名/服务名:版本号,别搞什么myimage、test123这种 - 标签策略要明确——我建议用语义化版本号,比如
v1.2.3,别只用latest - 定期清理旧镜像——保留最近N个版本就够了,太老的该删就删
- 镜像扫描不能少——用Trivy或者Clair扫描漏洞,高危的镜像直接拒绝部署
我曾经遇到过一件事:有个同事把生产环境的镜像标签写成了 latest,结果第二天部署的时候拉到了旧版本,线上挂了半小时。从那以后,我们强制要求所有生产镜像必须用具体版本号。
4.5 实战:一个完整的Dockerfile示例
最后,我给大家展示一个我在PaaS平台上实际用过的Dockerfile。这是一个Node.js应用,包含了今天讲的所有最佳实践:
# 第一阶段:依赖安装和构建
FROM node:16.13.0-alpine AS builder
LABEL maintainer="devops@example.com"
WORKDIR /app
# 先复制依赖文件,利用缓存
COPY package*.json ./
RUN npm ci --only=production && \
npm cache clean --force
# 复制源码并构建
COPY . .
RUN npm run build
# 第二阶段:运行环境
FROM node:16.13.0-alpine AS runner
# 安全相关:使用非root用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# 只复制构建产物和运行依赖
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
EXPOSE 3000
USER nodejs
CMD ["node", "dist/main.js"]
这个Dockerfile有几个亮点:用了多阶段构建、固定了基础镜像版本、合并了RUN指令、用了非root用户、加了健康检查。嗯,基本上该有的都有了。
避坑指南:我曾经在构建时用了 npm install 而不是 npm ci,结果因为package-lock.json和package.json不一致,构建出来的镜像在测试环境跑得好好的,上了生产就报错。后来我强制团队用 npm ci,再也没出过这种问题。
好了,这一章的内容就到这里。容器化看起来简单,但真正用好还是需要一些经验的。下一章咱们聊聊CI/CD流水线的设计原则,到时候我会分享一些我在PaaS平台上踩过的坑和总结的经验。