Dockerfile 编写速查表 - Docker 镜像构建指令大全
面向需要把应用打包成镜像的开发者。Dockerfile 的价值不在一句句清单,而在层顺序、构建缓存与多阶段构建这些组合规则——顺序不当会每次全量重编、体积暴涨,不加 USER 会让容器以 root 运行增加被攻面。读完能写出一份构建快速、运行镜像精简,且不把密钥和源码误打进镜像的 Dockerfile,并理解 ADD/COPY、ENV/ARG 为何要按场景区分。
典型使用场景
面向需要写 Dockerfile 与优化镜像体积的开发者。核心技巧只有两条但威力最大:多阶段构建(编译期装依赖与工具链,运行期只带产物,可把镜像从 GB 级压到几 MB)与分层缓存(先 COPY 依赖清单再装依赖,代码改动就不会击穿安装层)。本表按基础指令、环境变量与参数、文件操作、多阶段构建、入口点、构建优化、完整示例七组组织,每个示例都可直接对照修改。
基础指令 7
FROM node:18-alpineFROM scratchWORKDIR /appCOPY package*.json ./RUN npm ciEXPOSE 3000CMD ["node", "app.js"]环境变量与参数 7
ENV NODE_ENV=productionENV PATH="/app/bin:$PATH"ARG VERSION=1.0ARG NPM_TOKENLABEL maintainer="email@example.com"USER appuserHEALTHCHECK --interval=30s CMD curl -f http://localhost/ || exit 1文件操作 5
COPY src/ ./src/COPY --chown=node:node . .COPY --chmod=755 entrypoint.sh /entrypoint.shADD https://example.com/file.tar.gz /tmp/ADD file.tar.gz /app/多阶段构建 5
FROM node:18 AS builderCOPY --from=builder /app/dist ./distCOPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/FROM golang:1.21 AS builderFROM alpine:3.18入口点 ENTRYPOINT 5
CMD ["node", "app.js"]ENTRYPOINT ["node"]ENTRYPOINT ["node"]
CMD ["app.js"]SHELL ["/bin/bash", "-c"]ENTRYPOINT ["./docker-entrypoint.sh"]构建优化 6
COPY package*.json ./RUN npm ci && npm cache clean --forceRUN apk add --no-cache curl.dockerignoreRUN --mount=type=cache,target=/root/.npm npm ciFROM node:18-alpine完整示例 3
FROM node:18 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/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
CMD ["python", "main.py"]FROM golang:1.21 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server .
FROM alpine:3.18
COPY --from=builder /app/server /app/server
CMD ["/app/server"]参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
FROM node:18-alpine | 指定基础镜像,alpine 体积约为完整版 1/5 | FROM node:18-alpine |
WORKDIR /app | 设置工作目录,不存在会自动创建 | WORKDIR /app |
CMD ["node","app.js"] | 默认启动命令,exec 形式可正确接收 SIGTERM | CMD ["node", "app.js"] |
ENTRYPOINT ["node"] | 固定入口点,不被 run 参数覆盖 | ENTRYPOINT ["./docker-entrypoint.sh"] |
ENV | 设置环境变量,构建期与运行期都可用 | ENV NODE_ENV=production |
ARG | 仅构建期可用的参数 | ARG VERSION=1.0 |
COPY --chown | 复制时指定属主,避免产物为 root | COPY --chown=node:node . . |
ADD vs COPY | ADD 可下载 URL 与自动解压,普通文件用 COPY | ADD file.tar.gz /app/ |
多阶段 AS builder | 命名构建阶段供后续引用 | FROM node:18 AS builder |
COPY --from | 从构建阶段或外部镜像复制产物 | COPY --from=builder /app/dist ./dist |
USER | 切换运行用户降低容器权限 | USER appuser |
HEALTHCHECK | 健康检查,连续失败标记 unhealthy | HEALTHCHECK --interval=30s CMD curl -f http://localhost/ || exit 1 |
.dockerignore | 排除 node_modules、.git 等不需要进镜像的文件 | .dockerignore |
--mount=type=cache | BuildKit 缓存挂载,跨构建复用包缓存 | RUN --mount=type=cache,target=/root/.npm npm ci |
易错点与避坑指南
现象代码改一行就要重装全部依赖,构建非常慢。
原因COPY . . 放在安装依赖之前,代码变更击穿了安装层的缓存。
处置先 COPY package*.json ./ 再 RUN npm ci,装完再 COPY 剩余代码。
现象容器 stop 时要等十秒才退出。
原因用了 shell 形式 CMD,shell 作为 PID 1 不转发信号给子进程。
处置改用 exec 形式 CMD ["node", "app.js"],让应用直接作为 PID 1 接收 SIGTERM。
现象docker run 传入的参数覆盖了镜像里指定的启动程序。
原因用了 CMD 而非 ENTRYPOINT,run 后面的参数会整体替换 CMD。
处置ENTRYPOINT 定住程序、CMD 只给默认参数:ENTRYPOINT ["node"] 加 CMD ["app.js"]。
现象镜像里能看到密码明文,删了也还在。
原因--build-arg 传的密钥会留在构建历史中,即使后续层删掉也能翻出来。
处置改用 BuildKit secrets(RUN --mount=type=secret);已推送的镜像需轮换密钥。
现象镜像体积高达数 GB,远超预期。
原因单阶段构建把编译工具链和源码都留在最终镜像里。
处置改多阶段构建:builder 阶段装依赖编译,最终阶段只 COPY 产物。
现象容器内文件属主是 root,应用进程读写自己的目录报权限错。
原因COPY 默认以 root 属主写入镜像。
处置COPY --chown=node:node . .,并配合 USER 切换到非 root 用户运行。
现象把 node_modules 打进了镜像,本机平台的二进制在容器里跑不起来。
原因构建上下文没排除 .dockerignore 里的 node_modules。
处置在 .dockerignore 中排除 node_modules,让镜像内用 npm ci 安装对应平台的版本。
现象容器启动即退出,日志里看不出原因。
原因启动命令路径错误或文件不可执行,Dockerfile 本身不报错。
处置用 docker run --rm -it <image> sh 进入容器手动执行;检查 WORKDIR 与 CMD 路径是否匹配。
排障路径
1进容器手动排查启动失败
docker run --rm -it <image> sh镜像不可交互时可加 --entrypoint sh;手动跑一次启动命令能看到真实报错。
2优化构建缓存命中
COPY package*.json ./依赖清单单独成层,装完依赖后再 COPY 代码,代码改动不再触发重装。
3多阶段构建只取产物
COPY --from=builder /app/dist ./dist最终镜像不含源码与工具链,体积通常能降一个数量级。
4BuildKit 缓存挂载加速安装
RUN --mount=type=cache,target=/root/.npm npm cinpm 包缓存跨构建复用,比每层重新下载快很多;需开启 BuildKit。
5确认最终镜像包含什么
docker history <image>逐层看大小来源,能直接定位是哪个 RUN / COPY 让体积暴涨。
命令示例
Node 多阶段构建:编译期装全依赖,运行期只带产物
FROM node:18 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/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]依赖文件先 COPY 再 npm ci,改业务代码只让源码层失效、依赖层继续命中缓存。这一层顺序直接决定构建耗时。
输出
STEP 5/12 : RUN npm run build ---> Running in 8f3a1c > app@1.0.0 build > vite build vite v5.0.0 building for production... ✓ 1423 modules transformed. dist/index.html 0.46 kB Successfully built in 12.34s
用 .dockerignore 排除无关内容
# .dockerignore
node_modules
.git
*.log
.env
dist没有 .dockerignore 时整个 node_modules 会被传进构建上下文,上下文动辄几百 MB,拖慢每次构建;顺带还有把 .env 打进镜像的风险。
ARG 与 ENV 的作用域差别
ARG VERSION=1.0 # 仅构建时可用,镜像里不存在
ENV NODE_ENV=production # 构建时与运行时都可用
RUN echo $VERSION $NODE_ENVARG 定义的变量不会自动进入后续 RUN 的环境,需要 ENV 才能在镜像里保留。切换构建阶段版本号用 ARG,运行时需要读的配置用 ENV。
输出
1.0 production
常见坑与注意事项
- 把 .env 或密钥写进 ARG/ENV 会永久留在镜像层里,即使后续层删掉也能被 docker history 翻出。
- COPY 顺序不当会让依赖层缓存失效,每次改代码都重装依赖,构建从秒级变分钟级。
- 单阶段构建把编译工具链和源码都留在最终镜像里,体积可能多出数百 MB;用多阶段构建只 COPY 产物。
- 以 root 运行容器时一旦被突破即获得容器内 root 权限;应创建非 root 用户并在 USER 指令切换。
- CMD 会被 docker run 后的参数整体替换,命令写 CMD 里会让程序路径丢失;固定程序用 ENTRYPOINT。
提示
- 每条 RUN/COPY/ADD 指令都会创建新层,尽量合并 RUN 命令减少层数。
- 多阶段构建可以大幅减小最终镜像体积,适合编译型语言如 Go、Rust、Java。
- 使用 .dockerignore 排除 node_modules、.git、.env 等不需要构建的文件。
- CMD 和 ENTRYPOINT 区别:CMD 可被 docker run 参数覆盖,ENTRYPOINT 是固定的。
- 优先用 exec 形式 ["cmd","arg"] 而非 shell 形式,前者能正确接收 SIGTERM 信号,避免容器停止超时。
- 别用 --build-arg 传密码或密钥,它会留在镜像构建历史里,用 BuildKit secrets 方案更安全。
常见问题
为什么 Dockerfile 里要调整 COPY 的顺序?
因为每个 RUN/COPY/ADD 都会生成一层缓存,上层变了下面全部作废。要先 COPY package*.json ./ 再 RUN npm ci,这样改业务代码只会让源码层失效,依赖层继续命中缓存。顺序反过来的话每改一次代码就要重装全部依赖,构建从几秒变成几分钟。
多阶段构建解决了什么问题?
把构建期依赖和运行期依赖分开:builder 阶段装完整工具链完成编译,最终阶段只 FROM 一个精简基础镜像并 COPY --from=builder 拷产物。这样编译期的编译器、头文件、开发依赖都不会进入最终镜像。对 Node 项目效果最明显,运行镜像可以只剩 dist 与 node_modules。
ARG 和 ENV 有什么区别?
ARG 是构建时参数,可用 --build-arg 传入,但构建结束后就消失;ENV 既是构建时也是运行时环境变量,最终镜像里能读到。切换构建阶段版本号这种一次性的用 ARG,需要容器运行时可访问的(如 NODE_ENV)用 ENV。一个常见坑是 ARG 定义的变量不会自动进入后续 RUN 的环境,需要配 ENV 才能在镜像里保留。
CMD 和 ENTRYPOINT 怎么配合?
ENTRYPOINT 固定执行哪个程序,不容易被 docker run 的参数覆盖;CMD 提供默认参数,被 run 后面的参数替换。常见写法是 ENTRYPOINT ["node"] 加 CMD ["app.js"],于是默认执行 node app.js,而 docker run image other.js 会变成 node other.js。如果把命令写进 CMD,则整个会被替换,程序路径就丢了。
COPY 和 ADD 该用哪个?
普通文件复制一律用 COPY,它行为明确只做复制。ADD 多了两个能力:可以从 URL 下载文件,以及自动解压本地 tar 包。除了确实需要这两项(比如 ADD package.tar.gz /app/ 顺便解压),否则优先用 COPY,避免隐式解压带来的意外。另外要记得配 .dockerignore 排除 node_modules、.git 等不需要进镜像的内容。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。