第2章 Dockerfile编写基础归纳总结

写 Dockerfile 这事,说简单也简单,说坑也多。我刚入行那会儿,总觉得随便拼几行指令就能跑起来。后来被镜像大小和构建时长教做人了——一个基础镜像 1.2GB,每次构建要跑 15 分钟。嗯,从那以后我老老实实把 Dockerfile 的底子补了一遍。

这一章咱们就扎扎实实地捋一遍 Dockerfile 的编写要点。不是语法手册,而是实战里真正用得上的归纳。我分成指令速查、上下文管理、最佳实践和避坑四个块,最后画一张流程图把整个构建逻辑串起来。

Dockerfile 构建流程总览 FROM 基础镜像 ENV / ARG / LABEL 声明 WORKDIR 设置目录 COPY / ADD + RUN 安装依赖 CMD / ENTRYPOINT 每层指令都会生成一个新镜像层,层数越少、越小,构建越快

2.1 指令速查表——你迟早会翻回来的参考

下面这张表覆盖了 90% 的日常场景。我把它们按用途分了组,方便你对照。

指令作用典型示例
FROM指定基础镜像FROM python:3.11-slim
RUN执行构建命令RUN apt-get update && apt-get install -y curl
COPY复制文件到镜像COPY ./app /app
ADD类似COPY,支持URL和自动解压ADD https://example.com/file.tar.gz /tmp/
CMD容器默认启动命令(可被覆盖)CMD ["python", "app.py"]
ENTRYPOINT容器主入口(不可被覆盖)ENTRYPOINT ["docker-entrypoint.sh"]
WORKDIR设置工作目录WORKDIR /app
ENV设置环境变量ENV NODE_ENV=production
ARG构建时的参数(不会留在镜像里)ARG VERSION=1.0
EXPOSE声明端口(文档性质)EXPOSE 8080
VOLUME声明挂载卷VOLUME /data
LABEL添加元数据LABEL maintainer="dev@example.com"
USER切换运行用户USER appuser
SHELL修改默认shellSHELL ["/bin/bash", "-c"]
HEALTHCHECK容器健康检查HEALTHCHECK ...
ONBUILD触发指令(子镜像构建时执行)ONBUILD COPY . /app
我个人习惯把 LABEL、ENV、ARG 放在 FROM 之后,WORKDIR 之前。这样后续指令能直接用环境变量,也方便维护。

2.2 构建上下文与 .dockerignore——别把整个硬盘送进镜像

很多人写过这样的 Dockerfile:COPY . /app。结果镜像里塞满了 node_modules、.git、日志文件……我曾经接手一个项目,镜像里竟然有 200MB 的 .git 历史。解决方法很简单——写好 .dockerignore。

# .dockerignore 示例
node_modules
.git
*.log
.env
.gitkeep
__pycache__
dist
.editorconfig
.idea

.dockerignore 的语法和 .gitignore 基本一样。它在构建上下文发送给 Docker daemon 之前就把文件过滤掉了。你想想看,一个精简的上下文能省多少构建时间?

把构建上下文控制在 50MB 以内。如果项目有大量静态资源,考虑用多阶段构建分离。

2.3 最佳实践——我踩过的坑和攒下来的习惯

  1. 尽量用官方基础镜像:比如 python:3.11-slim 而不是 ubuntu:latest 然后自己装 Python。官方镜像经过安全审核,层数也优化过。
  2. 合并 RUN 指令:每一条 RUN 都会创建一个层。我建议把相关的 apt-get、pip install 串成一条命令,用 && 连接,最后清理缓存。
  3. 把不常变的 COPY 放前面:比如先 COPY package.json 和 requirements.txt,安装依赖,最后再 COPY 源代码。这样当代码改了,依赖层可以命中缓存。
  4. 使用多阶段构建:编译阶段用完整镜像,运行阶段用精简基础镜像。这样最终镜像只包含运行时所需文件。

举个例子,一个 Node.js 应用的多阶段构建会是这样:

# 第一阶段:编译
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 第二阶段:运行
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --production
EXPOSE 3000
CMD ["node", "dist/server.js"]
注意第二阶段只复制了构建产物和生产依赖,node_modules 里的开发包全被过滤掉了。最终镜像从 1.2GB 降到了 180MB,我亲自试过。

2.4 避坑指南——这些错误我至少犯过两次

不小心留下了敏感信息

我曾经在 Dockerfile 里直接写 ENV DB_PASSWORD=123456,结果推到了公共仓库。后来所有密码都改用构建参数或 Secret。

# 错误做法
ENV DB_PASSWORD=supersecret

# 正确做法:通过 --build-arg 传进来,运行时用环境变量文件
docker build --build-arg DB_PASSWORD=xxx .
docker run -e DB_PASSWORD=xxx ...

ADD 自动解压的副作用

ADD 会把本地的 tar.gz 文件自动解压到镜像里。如果你只想复制压缩包本身,得用 COPY。我有个同事因此多折腾了半小时。

CMD 和 ENTRYPOINT 混用导致参数丢失

如果你的 Dockerfile 同时写了 CMD 和 ENTRYPOINT,CMD 会被当成 ENTRYPOINT 的默认参数。想不清楚的时候,记住一条:把主命令放 ENTRYPOINT,默认参数放 CMD。

ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["--help"]

总结一下 Dockerfile 的黄金规则:

  • 每条指令只做一件事,但 RUN 指令内部可以串多条命令(减少层数)。
  • 优先级:多阶段构建 > 合并 RUN > 清理缓存。
  • .dockerignore 必须写,不然小心把宿主机全家福装进去。
  • 用户敏感信息用 ARG 或 secret,别硬编码。
  • 用 docker history 查看镜像层,检查有没有意外内容。