第2章 Dockerfile编写基础归纳总结
写 Dockerfile 这事,说简单也简单,说坑也多。我刚入行那会儿,总觉得随便拼几行指令就能跑起来。后来被镜像大小和构建时长教做人了——一个基础镜像 1.2GB,每次构建要跑 15 分钟。嗯,从那以后我老老实实把 Dockerfile 的底子补了一遍。
这一章咱们就扎扎实实地捋一遍 Dockerfile 的编写要点。不是语法手册,而是实战里真正用得上的归纳。我分成指令速查、上下文管理、最佳实践和避坑四个块,最后画一张流程图把整个构建逻辑串起来。
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 | 修改默认shell | SHELL ["/bin/bash", "-c"] |
HEALTHCHECK | 容器健康检查 | HEALTHCHECK ... |
ONBUILD | 触发指令(子镜像构建时执行) | ONBUILD COPY . /app |
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 之前就把文件过滤掉了。你想想看,一个精简的上下文能省多少构建时间?
2.3 最佳实践——我踩过的坑和攒下来的习惯
- 尽量用官方基础镜像:比如
python:3.11-slim而不是ubuntu:latest然后自己装 Python。官方镜像经过安全审核,层数也优化过。 - 合并 RUN 指令:每一条 RUN 都会创建一个层。我建议把相关的 apt-get、pip install 串成一条命令,用
&&连接,最后清理缓存。 - 把不常变的 COPY 放前面:比如先 COPY
package.json和requirements.txt,安装依赖,最后再 COPY 源代码。这样当代码改了,依赖层可以命中缓存。 - 使用多阶段构建:编译阶段用完整镜像,运行阶段用精简基础镜像。这样最终镜像只包含运行时所需文件。
举个例子,一个 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"]
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查看镜像层,检查有没有意外内容。