Dockerfile 参数解析
约 12800 字大约 43 分钟
2025-10-04
Dockerfile
镜像的定制实际上就是定制每一层所添加的配置、文件。
如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
一个 node 项目的Dockerfile
# ===============================
# Step 1: 使用官方 Node.js 镜像作为基础镜像
# 这里用的是 Node 20 LTS,带有 npm
# ===============================
FROM node:20
# ===============================
# Step 2: 设置工作目录
# 所有后续命令都在 /app 目录下执行
# ===============================
WORKDIR /app
# ===============================
# Step 3: 复制 package.json 和 package-lock.json
# 先复制依赖文件,可以利用 Docker 缓存加速构建
# ===============================
COPY package*.json ./
# ===============================
# Step 4: 安装依赖
# npm install 会安装所有依赖包
# ===============================
RUN npm install
# ===============================
# Step 5: 复制整个项目文件
# 把源代码复制到容器内
# ===============================
COPY . .
# ===============================
# Step 6: 编译 TypeScript
# NestJS 默认使用 TypeScript
# ===============================
RUN npm run build
# ===============================
# Step 7: 暴露应用端口
# NestJS 默认端口 3000
# ===============================
EXPOSE 3000
# ===============================
# Step 8: 设置容器启动命令
# 使用编译后的 JS 文件启动应用
# ===============================
CMD ["node", "dist/main.js"]docker build -t [镜像名称/版本号] .FROM
构建一个镜像的第一步,就是选择一个基础镜像,这个基础镜像就是我们要定制的镜像的起点。
在这个基础镜像上,我们可以添加文件、安装软件、配置环境,从而一步步构建出我们想要的镜像。
而选择基础镜像的指令,就是 FROM,一个 Dockerfile 必须以 FROM 开头。
比如构建一个 Java 应用的镜像,我们可以选择一个带有 JDK 的官方镜像作为基础,比如 openjdk:17-jdk-slim,然后在这个基础镜像上安装我们应用需要的其他软件、复制应用文件、配置环境变量等。
再或者,我们要构建一个 Node.js 应用的镜像,可以选择 node:20 作为基础镜像,Python 应用可以选择 python:3.11-slim,Ruby 应用可以选择 ruby:3.2,Go 应用可以选择 golang:1.21,等等。
甚至可以选择一些更广阔的基础镜像,比如 ubuntu:20.04、debian:bookworm-slim、alpine:3.18,然后在这些基础镜像上手动安装 JDK、Node.js、Python 等运行环境。
Docker 有一个特殊的镜像 scratch,它是一个空镜像,没有任何文件系统。它通常用于构建极简的基础镜像,比如只包含一个静态编译的二进制文件,没有任何额外的依赖。
COPY
格式:
COPY [--chown=<user>:<group>] <源路径>... <目标路径>COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
一种类似于命令行,一种类似于函数调用。
COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。比如:
COPY package.json /usr/src/app/<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match规则,如:
COPY hom* /mydir/
COPY hom?.txt /mydir/<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。
目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。
在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。
语法示例:
COPY --chown=user:group src/ /dest/
COPY --chown=uid:gid src/ /dest/COPY --chown=55:mygroup files* /mydir/ # 用户是 UID 55 所属组是 mygroup
COPY --chown=bin files* /mydir/ # 用户是 bin 所属组 默认使用 bin 用户的主组
COPY --chown=1 files* /mydir/ # 用户是 UID 1 所属组 默认使用 UID 1 的主组
COPY --chown=10:11 files* /mydir/ # 用户是 UID 10 所属组是 GID 11注意:
用户/组必须存在
- 使用用户名或组名时,容器里必须有这个用户或组,否则构建会报错
- 使用 UID/GID 可以避免依赖存在的用户名,但 GID 也最好是容器里已存在或你自己创建的
主组默认行为
- 如果只写
--chown=user,所属组会默认用这个用户的主组
如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。
ADD
ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。
比如 <源路径> 可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。
对于 ADD 指令下载的远程文件,Docker 不会自动修改权限,默认权限通常为 644,如果需要其他权限(例如 600),需要额外增加一条 RUN chmod 指令进行调整。此外,如果下载的是压缩包,Docker 不会自动解压,需要额外增加 RUN 指令手动解压。
所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。
ADD 虽然能下载文件,但 权限不会自动设置,远程压缩包不会自动解压
每次 ADD 或 RUN 都会增加一层镜像,如果你先 ADD 再 RUN chmod,再 RUN 解压,就多了好几层,镜像会臃肿
使用 RUN + wget/curl && chmod && 解压 && rm,一条 RUN 链接所有操作,只增加一层,下载、权限、解压、清理都在同一条指令里,并且清理临时文件不会留在中间层
RUN wget -O /tmp/app.tar.gz https://example.com/app.tar.gz && \
tar -xzf /tmp/app.tar.gz -C /app && \
chmod 600 /app/config.yml && \
rm /tmp/app.tar.gz一条 RUN 完成:下载,解压,修改权限,删除临时文件,并且只产生一层镜像,比多条 RUN 更轻量
总结:
在 Dockerfile 中,ADD 可以自动解压本地的 tar、tar.gz、tar.bz2、tar.xz 压缩文件到目标路径,并尽量保留目录结构和元数据;
但对于远程 URL 下载的压缩包,ADD 不会自动解压,也不会修改权限,默认权限通常为 644,如果需要其他权限或解压操作,需要额外增加 RUN 指令手动处理。因此,对于远程文件或压缩包,更推荐使用 RUN + wget/curl && chmod && 解压 && 清理 的方式,一条命令完成所有操作,更可控且镜像更轻量。
在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu 中:
FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...从 scratch + ADD rootfs 与直接 FROM ubuntu 的差别在于控制权和精简程度:前者可以完全自定义每一层内容,更轻量或者用于教学/实验,后者方便、快速、官方维护,但可能包含额外不必要的层和包。
在 Dockerfile 中,COPY 和 ADD 都可以复制文件到镜像,但官方最佳实践推荐:
尽量使用 COPY
- 语义明确,只复制文件或目录
- 行为简单,可预测
- 不会意外触发自动解压或下载
仅在需要自动解压本地 tar/gzip/bzip2/xz 文件时使用 ADD
- ADD 会自动解压本地压缩包
- 对远程 URL 下载的文件不会自动解压,但可能增加镜像层,影响构建缓存
权限与用户设置
- COPY 和 ADD 可配合
--chown=<user>:<group>设置文件所属用户和组 <user>和<group>可以是名称或 UID/GID
注意构建缓存
- ADD 的自动解压或远程下载功能可能会使缓存失效,导致构建变慢
CMD
CMD 指令的格式和 RUN 相似,也是两种格式:
shell格式:CMD <命令>exec格式:CMD ["可执行文件", "参数1", "参数2"...]- 参数列表格式:
CMD ["参数1", "参数2"...]。在指定了ENTRYPOINT指令后,用CMD指定具体的参数。
容器不是虚拟机,而是进程
- 容器就是一个运行在宿主机上的进程,它依赖镜像提供文件系统和运行环境
- 容器启动时必须指定主进程及其参数,这就是容器存在的核心
RUN 指令
- 用途:在镜像构建阶段执行命令
- 作用:安装软件、编译代码、处理文件等
- 执行时机:镜像构建阶段
- 注意事项:每条 RUN 会生成一层镜像,可写多条
RUN apt-get update && apt-get install -y curl
RUN npm installCMD 指令
用途:指定容器启动时默认执行的主进程命令
执行时机:容器运行阶段
特性:
- 可以被
docker run <image> <command>替换 - 镜像中只能有一条 CMD,多条会覆盖前面的
- 可以被
格式:
exec(JSON)格式:推荐使用,信号转发正确
CMD ["npm", "start"] CMD ["nginx", "-g", "daemon off;"]shell 格式:会被包装为
sh -c "...",支持环境变量和管道CMD echo $HOME # 实际执行为 CMD ["sh", "-c", "echo $HOME"]
前台 和 后台进程
容器没有后台服务的概念,主进程就是容器的核心
主进程退出,容器就退出
避免用 service nginx start 或 systemd 启动后台服务
- 因为主进程变成了
sh,后台服务结束后容器立即退出
正确做法:直接执行可执行程序并以前台形式运行
CMD ["nginx", "-g", "daemon off;"]详情
- 容器的本质上不是虚拟机,没有完整的 init/systemd/systemctl 管理后台服务
- 容器就是一个运行在宿主上的进程空间,主进程就是容器的核心
- 主进程退出,容器退出
不需要用系统服务去启动,比如 service nginx start 或 systemd
直接用 CMD 或 ENTRYPOINT 指定项目的启动命令
- 项目本身就是容器的主进程
- 必须以前台形式运行,否则主进程退出,容器随之退出
# 正确示例:Node.js 应用
CMD ["npm", "start"]
# 正确示例:Nginx
CMD ["nginx", "-g", "daemon off;"]原因总结
- 容器的生命周期依赖主进程
- 后台服务模式(像 VM 里那样)在容器里会导致主进程退出后容器立即停止
- 所以在容器里 每个服务都应以主进程形式启动
总结:容器运行项目时不依赖系统去调起服务,直接 CMD 指定启动命令即可,项目本身就是容器的主进程,以前台形式运行。
容器的主进程以前台形式运行时,它的 标准输出 (stdout) 和 标准错误 (stderr) 会直接挂在容器上
这就是为什么每次使用 docker run -it 或 docker logs <container> 都能看到应用的实时日志
docker run -it my-node-app
# 终端会直接显示 npm start 的输出如果主进程不是前台运行(比如后台启动服务),日志不会直接输出到容器 stdout,甚至容器会立即退出
这是前台运行的直接体现。
RUN 与 CMD 区别总结
| 指令 | 执行阶段 | 作用 | 注意事项 |
|---|---|---|---|
| RUN | 构建镜像 | 安装软件、构建项目、修改文件 | 每条生成镜像层,可多条 |
| CMD | 容器启动 | 默认启动命令(主进程) | 只能有一条,多条会覆盖;可被 docker run 覆盖 |
RUN 用于镜像构建阶段执行命令,CMD 用于容器启动阶段指定默认主进程命令,容器以主进程为核心,必须以前台形式运行;CMD 优先使用 exec 格式确保信号转发正确。
ENTRYPOINT
ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。
ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。
当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:
<ENTRYPOINT> "<CMD>"ENTRYPOINT 与 CMD 的组合:让镜像像命令一样使用
单独使用 CMD 的局限性
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
CMD ["curl", "-s", "http://myip.ipip.net"]镜像默认执行 curl -s http://myip.ipip.net
运行时如果直接追加参数,如 docker run myip -i
- Docker 会用
-i替换原 CMD - 因为
-i不是可执行命令,会报错:executable file not found
注
跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。
解决方式只能完整重写命令:
docker run myip curl -s http://myip.ipip.net -i不够方便
使用 ENTRYPOINT + CMD 组合
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["curl", "-s", "http://myip.ipip.net"]ENTRYPOINT 固定主命令为 curl -s http://myip.ipip.net
运行时追加参数(CMD 或 docker run <image> <args>)会作为参数传给 ENTRYPOINT
docker run myip # 输出默认 IP
docker run myip -i # 输出带 HTTP 头信息这样就可以让镜像 像命令一样使用,而不必每次手动完整输入命令
总结原则
- ENTRYPOINT:固定主命令,确保核心行为不被覆盖
- CMD:提供默认参数,可在运行时追加或覆盖参数
- 效果:运行时可以灵活传递参数,同时保持主命令不变,使镜像像可执行命令一样使用
通过 ENTRYPOINT + CMD 组合,可以让镜像像命令一样使用,ENTRYPOINT 固定主命令,CMD 提供默认参数,运行时追加参数会传给主命令,而不会覆盖它。
ENTRYPOINT + CMD 可以把镜像变成一个“命令行工具”,让容器不仅是环境,更像一个可执行的 CLI 应用。
ENV
格式有两种:
ENV <key> <value>ENV <key1>=<value1> <key2>=<value2>...
这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。
定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 node 镜像 Dockerfile 中,就有类似这样的代码:
ENV NODE_VERSION 7.2.0
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
&& grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
&& tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
&& rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs在这里先定义了环境变量 NODE_VERSION,其后的 RUN 这层里,多次使用 $NODE_VERSION 来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0 即可,Dockerfile 构建维护变得更轻松了。
下列指令可以支持环境变量展开(将环境变量的值替换到指令中使用的地方): ADD、COPY、ENV、EXPOSE、FROM、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD、RUN。
可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 Dockerfile 制作更多的镜像,只需使用不同的环境变量即可。
ARG
格式:ARG <参数名>[=<默认值>]
构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。
换句话说,ARG 就像是编译时的变量,而 ENV 是运行时的变量。
Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。
灵活的使用 ARG 指令,能够在不修改 Dockerfile 的情况下,构建出不同的镜像。
ARG 指令有生效范围,如果在 FROM 指令之前指定,那么只能用于 FROM 指令中。
ARG DOCKER_USERNAME=library
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo ${DOCKER_USERNAME}使用上述 Dockerfile 会发现无法输出 ${DOCKER_USERNAME} 变量的值,要想正常输出,你必须在 FROM 之后再次指定 ARG
# 只在 FROM 中生效
ARG DOCKER_USERNAME=library
FROM ${DOCKER_USERNAME}/alpine
# 要想在 FROM 之后使用,必须再次指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}对于多阶段构建,尤其要注意这个问题
# 这个变量在每个 FROM 中都生效
ARG DOCKER_USERNAME=library
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo 1
FROM ${DOCKER_USERNAME}/alpine
RUN set -x ; echo 2对于上述 Dockerfile 两个 FROM 指令都可以使用 ${DOCKER_USERNAME},对于在各个阶段中使用的变量都必须在每个阶段分别指定:
ARG DOCKER_USERNAME=library
FROM ${DOCKER_USERNAME}/alpine
# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}
FROM ${DOCKER_USERNAME}/alpine
# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}与 ENV 的区别
| 特性 | ARG | ENV |
|---|---|---|
| 生效阶段 | 构建时(build) | 运行时(run)和 构建时(build) |
| 默认可见范围 | 当前 Dockerfile 构建过程 | 最终镜像与运行时容器 |
是否出现在 docker inspect / docker run env | ❌ 不会 | ✅ 会 |
是否能在 docker build 时覆盖 | ✅ 使用 --build-arg | ❌ 不能(只能在容器启动时覆盖) |
是否能被 docker run -e 覆盖 | ❌ 不存在 | ✅ 可以 |
| 是否会被写入镜像层 | ✅(会出现在 docker history) | ✅(也会) |
| 是否适合保存敏感信息 | ❌(仍能在 history 看到) | ❌(同样能在 inspect/env 看到) |
ENV设置的环境变量在 Docker 构建阶段 和 容器运行阶段 都有效。ARG仅在 构建阶段 有效,且不会出现在最终镜像或运行时环境中。
无论是 ARG 还是 ENV:
都不安全存储密码、Token 等敏感数据。
原因:
ARG的值可以通过docker history查看;ENV的值可以通过docker inspect或docker exec env查看。
推荐安全方式:
- 使用 Docker Secret;
- 或通过 CI/CD 在运行时注入临时环境变量。
最佳实践:
ARG 用于“构建配置”,ENV 用于“运行配置”。 如果变量只影响构建过程,用 ARG; 如果变量要在运行时仍然可用,用 ENV; 如果两者都要,用 ARG + ENV 搭配。
使用 ARG 的场景(构建时变量)
ARG 主要用于构建阶段的逻辑控制,比如:
| 典型用途 | 示例 | 原因 |
|---|---|---|
| 指定基础镜像或依赖版本 | ARG NODE_VERSION=20 FROM node:${NODE_VERSION}-alpine | 在构建时确定版本,运行时不需要 |
| 传入构建上下文信息 | ARG TARGETARCH、ARG BUILDPLATFORM | 多架构构建(buildx)场景 |
| 控制构建行为 | ARG INSTALL_DEV=false RUN if [ "$INSTALL_DEV" = "true" ]; then npm i -D; fi | 仅影响构建行为 |
| 指定构建时用户名、镜像仓库等 | ARG DOCKER_USERNAME | 不需要留到容器中 |
特征:
- 构建时有效;
- 不会出现在运行容器的环境中;
- 可用
--build-arg临时覆盖; - 不适合存放密码、token。
使用 ENV 的场景(运行时变量)
ENV 用于容器运行时仍需存在的配置,例如:
| 典型用途 | 示例 | 原因 |
|---|---|---|
| 程序运行环境 | ENV NODE_ENV=production | 运行时需要识别环境模式 |
| 应用内部路径 | ENV APP_HOME=/usr/src/app | 构建时和运行时都需要 |
| 默认端口、语言、时区 | ENV LANG=C.UTF-8 ENV TZ=Asia/Shanghai | 容器长期需要 |
| 与 CMD/ENTRYPOINT 配合使用 | ENV CONFIG_PATH=/etc/app/config.json | 程序读取配置路径 |
特征:
- 构建时和运行时都能访问;
- 可被
docker run -e或.env文件覆盖; - 会写入镜像层;
- 不建议保存敏感信息(可通过 Secret 注入)。
ARG + ENV 联合使用(双阶段变量传递)
当你希望构建时通过 --build-arg 动态传入变量,并让它在运行时继续存在时,可以这样:
FROM node:20-alpine
# 构建时传入
ARG APP_VERSION=1.0.0
# 运行时也能用
ENV APP_VERSION=${APP_VERSION}
RUN echo "Building version: $APP_VERSION"
CMD ["node", "app.js"]构建:
docker build --build-arg APP_VERSION=2.1.0 -t myapp:2.1.0 .运行:
docker run --rm myapp:2.1.0容器中依然能访问:
echo $APP_VERSION
# 输出:2.1.0当变量既影响构建行为,又影响程序运行时,用 ARG 定义输入 → ENV 保存输出。
常见错误与反例
| 错误 | 问题 |
|---|---|
| 把密码写进 ARG 或 ENV | 这两者都会被 docker history 或 inspect 看到 |
| 想在 FROM 之后用 FROM 前的 ARG | 必须重新声明,否则为空 |
| 想在运行时访问 ARG | 不行,ARG 在镜像构建完就消失了 |
| 想在构建时访问 ENV(但写在 ENV 之前) | 无效,ENV 在定义之后才生效 |
VOLUME
格式为:
VOLUME ["<路径1>", "<路径2>"...]VOLUME <路径>
容器在运行时产生的数据(如日志、数据库文件)如果直接写入容器文件系统,会造成:
- 容器存储层膨胀;
- 删除容器后数据丢失;
- 镜像更新时无法继承旧数据。
因此,动态数据必须与容器生命周期解耦,解决方案:将数据保存到 卷(Volume) 中。
VOLUME /data意思是:“在容器中声明一个挂载点 /data,告诉 Docker 这个目录应该被当作一个**数据卷(Volume)**来处理。”
注意:它不会自动映射到宿主机的 /data 目录。
实际情况上,如果你只是写了:
docker run myappDocker 会自动为 /data 创建一个匿名卷(anonymous volume),它位于宿主机的:
/var/lib/docker/volumes/<随机ID>/_data也就是说,宿主机的路径是随机的,而不是 /data。
如果你想让容器的 /data 映射到宿主机的 /data,必须显式写:
docker run -v /data:/data myapp这样宿主机的 /data 和容器的 /data 才是一一对应的。
并且在 Dockerfile 里声明 VOLUME /data:告诉别人这个目录有持久化意义。
然后在 docker run 或 Compose 中显式挂载路径:
volumes:
- ./data:/data这样可以控制宿主机的存储位置,不依赖匿名卷。
三种情况对比表
| 类型 | 示例命令 | 宿主机路径 | 管理方式 | 特征 |
|---|---|---|---|---|
| 命名卷(named volume) | -v mydata:/data | /var/lib/docker/volumes/mydata/_data | Docker 管理,可复用 | 左边是一个单词名称 |
| 匿名卷(anonymous volume) | Dockerfile: VOLUME /data 或 -v /data | /var/lib/docker/volumes/<随机ID>/_data | Docker 自动创建,不可复用 | 名称自动生成(哈希) |
| 绑定挂载(挂载卷)(bind mount) | -v /home/user/data:/data | /home/user/data(宿主机真实路径) | 用户自行管理 | 左边是路径(带 /) |
挂载卷就是用户手动指定宿主机目录挂载到容器目录,相当于完全控制存储路径。
示例:
# 使用匿名卷(Dockerfile 中的 VOLUME /data)
docker run -d myimage
# 使用命名卷,替代匿名卷
docker run -d -v mydata:/data myimage命名卷会覆盖掉 Dockerfile 中定义的匿名卷挂载。
可以用以下命令区分卷类型:
docker inspect <容器ID> | grep Source命名卷或匿名卷会在 /var/lib/docker/volumes/ 下, 绑定挂载会显示宿主机的真实路径(如 /home/user/data)。
注意:
匿名卷生命周期与容器一致,当容器被删除时,如果你没加 --volumes,匿名卷会残留在宿主机上,占用空间,可以用 docker volume prune 清理。
匿名卷一般用于临时数据或默认数据目录,而生产环境通常会显式使用命名卷。
最佳实践:
应用的动态数据目录(如数据库、日志、缓存等)应使用 VOLUME 声明;
在运行容器时,推荐使用命名卷或绑定挂载,例如:
docker run -d -v mydata:/var/lib/mysql mysql镜像应保持无状态,只包含程序和依赖;
卷负责持久化与共享数据,与容器生命周期解耦。
命名卷管理命令
显式创建命名卷
docker volume create mydata可选参数:
docker volume create --driver local --label project=demo mydata--driver:指定卷驱动,一般用 local
--label:给卷打标签,方便管理
查看卷列表
docker volume ls示例输出:
DRIVER VOLUME NAME
local mydata
local otherdata查看卷详情
docker volume inspect mydata输出包括:
- 卷的宿主机路径 (
Mountpoint) - 驱动信息
- 使用的容器(如果有)
删除卷
# 删除未被使用的卷
docker volume rm mydata
# 删除所有未使用卷
docker volume prune正在被容器使用的卷无法删除;
prune 会清理所有未使用的卷,谨慎执行。
使用卷挂载容器
docker run -d -v mydata:/data myapp如果卷不存在,Docker 会自动创建;
生产环境推荐提前显式创建卷,便于管理和监控。
EXPOSE
格式为 EXPOSE <端口1> [<端口2>...]
声明端口:告诉镜像使用者容器内哪个端口会提供服务。
EXPOSE 80
EXPOSE 443/tcp
EXPOSE 53/udp不启动服务:EXPOSE 只是声明端口,容器内部服务必须自己启动,EXPOSE 本身不会监听端口
与端口映射结合:
docker run -P:自动将所有 EXPOSE 声明的端口随机映射到宿主机端口。docker run -p <宿主端口>:<容器端口>:手动指定端口映射,公开容器端口给宿主或外界访问。
-p / -P 的区别
| 指令 | 功能 | 是否映射宿主端口 | 用途 |
|---|---|---|---|
| EXPOSE | 声明容器内部服务端口 | ❌ 否 | 文档化端口,帮助使用者了解容器提供的服务端口;配合 -P 自动随机映射 |
| -p <宿主端口>:<容器端口> | 映射宿主端口到容器端口 | ✅ 是 | 手动暴露服务端口,允许外部访问 |
| -P | 自动映射所有 EXPOSE 的端口 | ✅ 是 | 随机分配宿主端口,方便快速启动和测试 |
最佳实践
镜像中声明 EXPOSE:帮助使用者了解容器服务端口,方便文档化和团队协作。
运行容器时使用 -p 或 -P:
- 开发或测试环境可用
-P随机映射端口; - 生产环境应使用
-p <宿主端口>:<容器端口>,保证端口可控。
端口协议明确:TCP/UDP 需在 EXPOSE 中标明,否则默认 TCP。
EXPOSE 也可以声明多个端口:
EXPOSE 80 443EXPOSE 只是镜像元数据的一部分,在 docker inspect 时可以看到:
docker inspect <镜像ID> | grep ExposedPorts -A 3WORKDIR
格式为 WORKDIR <工作目录路径>
指定工作目录:设置 Dockerfile 后续所有 RUN、CMD、ENTRYPOINT、COPY、ADD 等命令的默认工作目录。
自动创建目录:如果指定的目录不存在,Docker 会帮你创建。
WORKDIR /app
RUN echo "hello" > world.txt上述示例中,world.txt 会生成在 /app 下。
常见错误示例
很多初学者会把 Dockerfile 当作普通 Shell 脚本写,例如:
RUN cd /app
RUN echo "hello" > world.txt- 问题原因:每个
RUN命令都是独立容器执行的临时环境,前一个命令的内存状态(如当前工作目录)不会影响下一条RUN。 - 结果:
world.txt不会出现在/app下,而是生成在默认工作目录/下。
相对路径用法
- WORKDIR 支持相对路径,相对路径是基于 上一条 WORKDIR:
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd- 输出路径为
/a/b/c。 - 多个 WORKDIR 可以连续叠加,相当于在前一个目录的基础上切换。
最佳实践:
尽量在 Dockerfile 开头设置 WORKDIR
- 可以保证后续所有命令在正确目录执行,避免错误。
相对路径小心使用
- 明确理解它是基于前一个 WORKDIR 的路径,不要写模糊路径。
与 COPY/ADD 配合
WORKDIR /app
COPY . .
RUN npm install复制操作和安装命令都会基于 /app 目录执行,更直观。
不要用 RUN cd 切换目录
- 每个 RUN 都是新容器,
cd的效果不会保留。
USER
格式:USER <用户名>[:<用户组>]
USER 用于切换容器内的执行用户。
影响后续层的所有指令,包括 RUN、CMD、ENTRYPOINT 等。
注意:切换的是容器内用户,与宿主机用户无关。
RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN echo "Hello" > /tmp/hello.txt上例中,后续 RUN 命令都会以 redis 用户身份执行,而不是 root。
常见误区
USER 不是 sudo/su:
USER是 Docker 构建镜像阶段或容器启动阶段的默认用户切换。- 不依赖 TTY,不会报错(不像直接在容器内用
su/sudo切换用户,可能因无终端或缺少密码而失败)。
切换用户启动服务的意义:
- 安全性:容器内的服务通常不建议以 root 用户运行,降低被攻破后的权限风险。
- 权限隔离:保证服务只能访问自己用户的文件和目录,防止误操作或安全漏洞。
- 符合最佳实践:许多官方镜像都会提供非 root 用户运行服务(如
nginx、redis、mysql)。
结合 gosu
- 有时需要在容器启动阶段用 root 用户做准备工作,但执行服务时使用非 root 用户。
gosu可以方便切换用户执行命令:
RUN groupadd -r redis && useradd -r -g redis redis
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true
CMD ["exec", "gosu", "redis", "redis-server"]- 作用:在容器启动时,以
redis用户启动服务,而构建时仍可用 root 完成安装、权限配置等操作。
容器内运行 root 的风险
如果容器内的服务以 root 用户 运行:
- 攻击者一旦突破容器,他们在容器里的权限就是 root。
- 可能发生的事情:
- 修改容器内文件:包括程序、配置、日志等。
- 读取敏感信息:比如挂载卷内的数据(如果卷挂载了宿主机目录,root 有权限访问)。
- 逃逸风险增加:在某些情况下,攻击者可能利用内核漏洞或挂载卷逃逸到宿主机,获取宿主 root 权限。
非 root 用户的安全优势
- 如果容器内服务以 普通用户 运行:
- 攻击者权限被限制在该用户能访问的范围内。
- 对容器内其他目录或挂载卷(需要更高权限的目录)无法随意修改。
- 减少了潜在的容器逃逸风险。
注意:这并不能完全保证安全,仍需结合最小权限原则、只挂载必要卷、限制能力(capabilities)、使用只读文件系统等措施。
核心理念:容器不是完全面的隔离,但以非 root 用户运行服务可以显著降低攻击面和潜在风险。
HEALTHCHECK
用于 检测容器运行状态,判断服务是否健康。
Docker 1.12 之前只能通过主进程是否退出判断容器状态,无法发现死锁或服务卡死等情况。
HEALTHCHECK 指令允许容器 主动自检,状态分为:
starting:健康检查刚开始,尚未判断。healthy:健康检查通过。unhealthy:连续失败超过重试次数。
语法
HEALTHCHECK [选项] CMD <命令> # 设置健康检查命令
HEALTHCHECK NONE # 屏蔽基础镜像已有的健康检查可选参数:
| 选项 | 说明 | 默认值 |
|---|---|---|
--interval=<间隔> | 两次健康检查之间的时间间隔 | 30s |
--timeout=<时长> | 健康检查命令运行超时时间 | 30s |
--retries=<次数> | 连续失败多少次判定为 unhealthy | 3 |
命令返回值约定:
0→ 健康1→ 不健康2→ 保留,不要使用
命令格式:
- Shell 格式:
CMD curl -fs http://localhost/ || exit 1 - Exec 格式:
CMD ["curl", "-fs", "http://localhost/"]
和 CMD、ENTRYPOINT 一样,HEALTHCHECK 只能出现一次,后续会覆盖前面的。
示例
构建带健康检查的 nginx 镜像:
FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
CMD curl -fs http://localhost/ || exit 1运行容器:
docker build -t myweb:v1 .
docker run -d --name web -p 80:80 myweb:v1查看状态:
docker container ls
# 初始状态:health: starting
# 几秒后:health: healthy如果连续失败超过 --retries,状态会变为 unhealthy。
查看健康检查日志:
docker inspect --format '{{json .State.Health}}' web | python -m json.tool使用场景与建议
场景:
- Web 服务、API 服务、数据库等进程可能“卡死”或“死循环”。
- 需要和 Docker 编排工具(如 Docker Swarm、Kubernetes)配合,实现自动重启或调度。
建议:
- 健康检查命令尽量轻量,避免对容器本身造成负载。
- 间隔和超时可根据实际业务调整,不必太频繁。
- 健康检查可以结合
CMD或ENTRYPOINT启动的主进程一起设计,确保检查覆盖关键功能。
补充
HEALTHCHECK 只是 容器自身状态检测,不会自动修复或重启容器。
在编排工具中,可结合容器状态做自动重启或流量切换:
- Docker Swarm:服务健康检查失败可触发重启。
- Kubernetes:LivenessProbe / ReadinessProbe 原理类似。
使用 HEALTHCHECK NONE 可以覆盖基础镜像自带的健康检查。
总结:HEALTHCHECK 是 Docker 提供的容器健康状态自检机制,通过执行指定命令定期判断服务状态,为自动化调度和高可用提供基础。
LABEL
作用:给镜像添加元数据(metadata),以键值对的形式存储。元数据可以用来描述镜像信息、作者、版本、文档地址等,方便管理、搜索和自动化工具使用。
格式:
LABEL <key>=<value> <key>=<value> ...注意:多个键值对可以写在同一行,也可以分多行写。
示例
# 作者信息
LABEL maintainer="yumengjianghu <yumeng@example.com>"
# 镜像相关信息
LABEL org.opencontainers.image.authors="yeasy"
LABEL org.opencontainers.image.version="1.0"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.source="https://github.com/yeasy/example"
LABEL org.opencontainers.image.documentation="https://yeasy.gitbooks.io"
LABEL org.opencontainers.image.title="My Docker Image"
LABEL org.opencontainers.image.description="这是一个示例 Docker 镜像"常用标签分类
| 类别 | 键示例 | 说明 |
|---|---|---|
| 作者信息 | maintainer, org.opencontainers.image.authors | 指明镜像的作者 |
| 文档 | org.opencontainers.image.documentation | 镜像文档或说明网址 |
| 源码 | org.opencontainers.image.source | 镜像对应的源码仓库 |
| 版本 | org.opencontainers.image.version | 镜像版本号 |
| 许可证 | org.opencontainers.image.licenses | 镜像使用的开源许可证 |
| 描述 | org.opencontainers.image.title, org.opencontainers.image.description | 镜像名称和描述 |
补充说明
- 元数据不会影响镜像运行
- LABEL 只是存储信息,不会改变容器行为或占用额外资源。
- 方便搜索和自动化
- Docker Hub、私有 Registry、CI/CD 工具都可以根据标签筛选、查找和管理镜像。
- 多行写法(可读性更好):
LABEL org.opencontainers.image.authors="yeasy" \
org.opencontainers.image.version="1.0" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.documentation="https://yeasy.gitbooks.io"总结:LABEL 指令是镜像的“名片”,存储作者、版本、文档等信息,便于管理、搜索和自动化流程,是 构建规范化镜像的最佳实践。
SHELL
作用:指定 RUN、CMD、ENTRYPOINT 执行命令时使用的默认 shell。
- Linux 默认 shell 是
["/bin/sh", "-c"] - Windows 默认 shell 是
["cmd", "/S", "/C"]
格式:
SHELL ["executable", "parameters"]注意:只有 exec 形式(JSON 数组形式)才可靠,推荐使用数组形式而非字符串形式。
示例
# 默认 shell
SHELL ["/bin/sh", "-c"]
RUN echo "Hello"; ls
# 启用错误退出和命令打印
SHELL ["/bin/sh", "-cex"]
RUN echo "Hello"; ls-c:执行命令字符串-e:遇到错误时退出-x:打印每条命令及参数(调试友好)
与 ENTRYPOINT / CMD 的关系
当 ENTRYPOINT 或 CMD 使用 shell 格式(字符串形式)时,SHELL 指定的 shell 会成为这两个指令的执行 shell:
SHELL ["/bin/sh", "-cex"]
# CMD 或 ENTRYPOINT 使用 shell 格式时
CMD nginx
ENTRYPOINT nginx等价于执行:
/bin/sh -cex -c "nginx"如果 CMD / ENTRYPOINT 使用 exec 形式(数组形式),SHELL 不会生效,直接执行指定的可执行文件。
使用场景和最佳实践
调试 RUN 命令
- 在构建镜像时,
-x参数方便查看每条命令执行情况 -e参数可以避免错误被忽略,防止构建成功但镜像不可用
确保 ENTRYPOINT/CMD 的一致性
- 当 ENTRYPOINT / CMD 使用 shell 格式时,可以通过 SHELL 指令指定执行行为(如开启
-e防止启动失败被忽略)
安全和可移植性
- exec 形式的 CMD / ENTRYPOINT 更安全,SHELL 不会影响
- 对于复杂命令,建议在 RUN 时用 SHELL 调试,但生产 CMD / ENTRYPOINT 尽量用 exec 形式
示例:调试 RUN 命令
SHELL ["/bin/sh", "-cex"]
RUN apt-get update && apt-get install -y curl \
&& curl -fs http://example.com || exit 1- 遇到任何失败都会立即停止构建
- 输出每条命令和执行结果,方便排查问题
ONBUILD
作用:在 Dockerfile 中设置触发指令,这些指令不会在当前镜像构建时执行,而是在基于该镜像的下游镜像构建时执行。
也就是说,ONBUILD 是一种 “延迟执行”的机制,把某些操作留给子镜像处理。
格式
ONBUILD <指令><指令>可以是 RUN、COPY、ADD 等绝大多数 Dockerfile 指令- 当你用这个镜像做 FROM 时,ONBUILD 指令才会触发
示例
# 父镜像 Dockerfile
FROM ubuntu:20.04
ONBUILD COPY . /app
ONBUILD RUN make /app然后我们写一个子镜像:
# 子镜像 Dockerfile
FROM myparentimage
RUN echo "Hello World"构建子镜像时,父镜像的 ONBUILD 指令会被触发,等价于:
COPY . /app
RUN make /app使用场景
构建模板镜像(Base image)
- 父镜像定义一些通用操作,如复制应用目录、安装依赖
- 子镜像只需要 FROM 父镜像,就自动继承这些操作
为下游镜像“约定行为”
- 比如一个语言运行环境镜像,可以约定下游镜像必须复制源代码并编译
注意事项
ONBUILD 指令只触发在下游镜像构建时
- 构建父镜像时不会执行
不要滥用 ONBUILD
- 容易造成“魔法行为”,让下游镜像构建变得不透明
- 不适合普通应用镜像,更多用于库镜像或模板镜像
常用组合
ONBUILD COPY . /app # 子镜像会自动复制当前目录
ONBUILD RUN pip install -r /app/requirements.txt # 安装依赖调试技巧
- 父镜像中可以写注释提醒下游开发者 ONBUILD 的作用
- 构建子镜像时,如果不想执行 ONBUILD,可以选择不继承父镜像,或者手动处理
ONBUILD 指令最佳实践
ONBUILD 虽然提供了延迟执行的机制,但在实际使用中需要谨慎。最佳实践如下:
适合模板镜像(Base Image)
- 用于为下游镜像约定通用操作,例如:复制源代码、安装依赖、执行初始化操作。
- 不适合普通应用镜像,否则容易造成“魔法行为”,下游构建难以理解。
尽量避免复杂逻辑
- ONBUILD 指令最好保持简单和明确
- 复杂的条件、参数化操作不适合使用 ONBUILD
替代方案:使用参数 + 条件 RUN
- 通过
ARG和环境变量控制构建逻辑,更灵活、易维护 - 示例:
# 父镜像 Dockerfile
ARG INSTALL_DEPS=true
RUN if [ "$INSTALL_DEPS" = "true" ]; then \
apt-get update && apt-get install -y curl; \
fi下游镜像可以传入不同参数决定是否安装依赖:
docker build --build-arg INSTALL_DEPS=false -t mychildimage .可维护性考虑
- ONBUILD 是隐式触发,难以调试
- 参数 + 条件逻辑明确,构建流程可控
ONBUILD 适合简单的模板镜像和固定操作的延迟执行场景; 更灵活、可维护的方案是使用 ARG / ENV + 条件 RUN,通过传参控制镜像构建逻辑,避免魔法行为,同时方便调试和扩展。
RUN
RUN 指令是用来执行命令行命令的。由于命令行的强大能力,RUN 指令在定制镜像时是最常用的指令之一。其格式有两种:
- shell 格式:
RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的RUN指令就是这种格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html- exec 格式:
RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。
虽然 RUN 可以像 Shell 脚本一样可以执行命令,但是 Dockerfile 中每一个指令都会建立一层,RUN 也不例外。
如果你有很多的命令需要执行,可以使用 && 把它们连接成一行,这样就只会创建一层,如果太长,可以使用反斜杠 \ 换行,甚至使用 # 注释来增加可读性。
并且注意在执行命令后清理不必要的文件,减少镜像体积,必须在同层中完成清理,否则清理操作会在新层中执行,前一层的文件依然存在,因为每一层都是只读的。
同层删除 → 文件在同一层被创建后又删除 → 从未进入最终镜像层 → 真正减小体积
跨层删除 → 删除操作在新的一层发生 → 上一层的文件依然存在,只是被标记为不可见 → 占用空间不减
构建镜像
为了构建一个镜像,需要在 Dockerfile 所在目录下运行 docker build 命令。
docker build [选项] <上下文路径/URL/->
docker build -t <镜像名称:标签> .. 表示当前目录,而 Dockerfile 就在当前目录,这个参数用于指定构建上下文,而不是 Dockerfile 所在目录。
即 Docker 引擎在构建镜像时可以访问的文件和目录,Docker 会把上下文目录下的所有文件打包并发送给 Docker 引擎,用于镜像构建。
COPY / ADD 指令 的源路径都是相对于上下文目录的。
超出上下文的路径无法被 Docker 引擎访问,例如:
COPY ../package.json /app # 无法工作如果文件不在上下文内,需要先移动或复制到上下文目录中。
上下文越大,docker build 上传给 Docker 引擎的包就越大,构建越慢,避免将整个硬盘或无关目录作为上下文,可以使用 .dockerignore 文件排除不需要的文件(语法类似 .gitignore)。
默认情况下:Dockerfile 名称为 Dockerfile,位于上下文目录中,可以用 -f 指定其他路径或名称的 Dockerfile:
docker build -t myimage -f ../Dockerfile.custom .docker build 还支持从 URL 构建,比如可以直接从 Git repo 中构建:
docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world这行命令指定了构建所需的 Git repo,并且指定分支为 master,构建目录为 /amd64/hello-world/,然后 Docker 就会自己去 git clone 这个项目、切换到指定分支、并进入到指定目录后开始构建。
docker build http://server/context.tar.gzDocker 会下载指定的 tar.gz 包,自动解压并将解压后的内容作为 构建上下文,默认会在上下文根目录寻找 Dockerfile
用途:远程构建非 Git 仓库内容
docker build - < context.tar.gz如果标准输入是 tar 压缩包(gzip / bzip2 / xz),Docker 会解压后将内容作为 构建上下文,可正常使用 COPY / ADD 指令
用途:通过管道或脚本提供上下文,而不依赖本地目录
docker build - < Dockerfile
# 或
cat Dockerfile | docker build -标准输入直接提供 Dockerfile 内容,没有上下文 ,不能使用 COPY / ADD 拷贝本地文件,适合临时或简短 Dockerfile 构建。
最佳实践:
docker build \
-f <Dockerfile路径> \ # 指定 Dockerfile 文件,默认是 ./Dockerfile
-t <镜像名称>:<标签> \ # 镜像名称和标签,例如 myapp:latest
--build-arg <参数名>=<值> \ # 可选,传入构建参数 ARG
--no-cache \ # 可选,不使用缓存重新构建
--pull \ # 可选,总是尝试拉取最新基础镜像
--progress=plain \ # 可选,构建输出显示方式
<上下文路径> # 构建上下文,一般是当前目录 .示例
docker build \
-f Dockerfile \
-t myapp:1.0.0 \
--build-arg APP_ENV=production \
--pull \
--progress=plain \
.解释:
-f Dockerfile:使用当前目录下的 Dockerfile-t myapp:1.0.0:给镜像打标签,方便管理和推送仓库--build-arg APP_ENV=production:传入构建参数--pull:拉取最新基础镜像,确保镜像更新--progress=plain:显示完整日志,方便调试.:上下文路径为当前目录
总是给镜像打标签 (-t <name>:<tag>)
大上下文目录可以使用 .dockerignore 排除不必要文件
尽量把不常变化的指令放在前面利用缓存,加快构建速度
对于敏感信息,不要直接写在 Dockerfile,使用 --build-arg 或构建时注入环境变量
多阶段构建
全部放入一个 Dockerfile:将所有的构建过程编包含在一个 Dockerfile 中,包括项目及其依赖库的编译、测试、打包等流程,这里可能会带来的一些问题,比如:
- 镜像层次多,镜像体积较大,部署时间变长
- 源代码存在泄露的风险
分散到多个 Dockerfile
事先在一个 Dockerfile 将项目及其依赖库编译测试打包好后,再将其拷贝到运行环境中,这种方式需要我们编写两个 Dockerfile 和一些编译脚本才能将其两个阶段自动整合起来,这种方式虽然可以很好地规避第一种方式存在的风险,但明显部署过程较复杂。
使用多阶段构建
为解决以上问题,Docker v17.05 开始支持多阶段构建 (multistage builds)。
使用多阶段构建我们就可以很容易解决前面提到的问题,并且只需要编写一个 Dockerfile:
FROM golang:alpine as builder
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/helloworld/
RUN go get -d -v github.com/go-sql-driver/mysql
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest as prod
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/go/helloworld/app .
CMD ["./app"]我们还可以使用 as 来为某一阶段命名,例如
FROM golang:alpine as builder例如当我们只想构建 builder 阶段的镜像时,增加 --target=builder 参数即可
docker build --target builder -t username/imagename:tag .构建时从其他镜像复制文件
上面例子中我们使用 COPY --from=0 /go/src/github.com/go/helloworld/app . 从上一阶段的镜像中复制文件,我们也可以复制任意镜像中的文件。
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf多阶段构建基本语法:
FROM <基础镜像> AS <阶段名>
...
COPY --from=<阶段名或序号> <源路径> <目标路径>AS <阶段名>:给阶段命名,便于后续 COPY 或单独构建
--from:从指定阶段或任意镜像复制文件
核心概念
多阶段构建就是在 同一个 Dockerfile 中定义多个构建阶段,每个阶段都是一个独立的镜像构建环境。最终镜像只保留运行所需的文件,而构建时产生的临时文件、源码、编译工具等不会进入最终镜像。
通俗理解:
就像“先在厨房里做好所有材料(builder 阶段),然后把最终成品搬到餐盘上(production 阶段)”,最终盘子里只放菜,不放调料、厨具或废料。
多阶段构建的好处:
- 减小镜像体积:不把编译工具和源码带入最终镜像
- 提高安全性:源码、临时文件和秘钥不会暴露在最终镜像中
- 方便管理:一个 Dockerfile 完成构建和打包,不需要额外脚本
- 可选择性构建:可通过
--target构建指定阶段
# 第一阶段:builder
FROM golang:alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .
# 第二阶段:生产运行
FROM alpine:latest AS prod
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]关键点:
AS <阶段名>:给阶段命名,便于后续引用COPY --from=<阶段名或序号>:从指定阶段复制文件- 每个阶段都是独立的镜像环境,二次构建阶段不会自动继承前一阶段的环境,只能拿文件
二次构建阶段细节
环境独立:
- 第二阶段的基础镜像可以与第一阶段完全不同
- 第二阶段无法访问第一阶段安装的包或命令(除非复制)
复制文件粒度灵活:
- 可以复制单个文件、目录,甚至从任意镜像中复制文件
- 例:
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
优化缓存和效率:
- 依赖不常改动的指令放前面,减少重复构建
- 源码、配置文件变动只影响最后几层
总结与最佳实践
- 保持最终镜像干净、轻量、只包含运行时依赖
- 给阶段命名清晰,比用数字序号更易维护
- 编译依赖、源码、测试文件只留在 builder 阶段
- 生产阶段只复制必要文件
- 适合编译型语言或前端打包(Go、C/C++、Java、Node)
通俗总结:多阶段构建就是“先在一个干净的厨房里把所有东西做出来,然后只把菜搬到盘子上”,这样最终镜像既小又干净,构建过程也可以灵活控制。
多架构
在 Docker 中,镜像和宿主机架构必须一致,例如:
- x86_64 Linux 宿主机只能运行 x86_64 Linux 镜像
- ARM64 Linux 宿主机只能运行 ARM64 Linux 镜像
如果直接在树莓派(ARM)上拉取 x86_64 镜像,容器会拉不到或无法运行。
传统做法是为不同架构构建不同镜像,例如:
username/test:x86_64username/test:arm64v8
但这种方式不够方便,用户需要自己选择架构镜像。
Docker 支持 Manifest 列表(manifest list),也称为 多架构镜像列表。
一个 manifest 列表包含同一镜像的不同架构版本
Docker 客户端根据宿主机架构自动选择合适的镜像
官方镜像(如 golang:alpine)就是这种方式
查看 manifest 列表:
docker manifest inspect golang:alpine输出中每个 manifest 对应一个特定架构:
"platform": {
"architecture": "arm64",
"os": "linux",
"variant": "v8"
}创建多架构镜像的流程
(1) 构建不同架构的镜像并推送
# x86_64 构建 & 推送
docker build -t username/x8664-test .
docker push username/x8664-test
# arm64v8 构建 & 推送
docker buildx build --platform linux/arm64 -t username/arm64v8-test .
docker push username/arm64v8-testbuildx 支持交叉架构构建,尤其适合在 x86_64 主机上构建 ARM 镜像。
(2) 创建 manifest 列表
docker manifest create username/test \
username/x8664-test \
username/arm64v8-testusername/test 将成为多架构镜像名
可使用 --amend 修改已有列表
(3) 标注 manifest 信息
docker manifest annotate username/test username/x8664-test \
--os linux --arch amd64
docker manifest annotate username/test username/arm64v8-test \
--os linux --arch arm64 --variant v8--os、--arch、--variant 用于指明每个镜像的架构信息
(4) 查看 manifest 列表
docker manifest inspect username/test输出中会显示不同架构的 digest 信息。
(5) 推送 manifest 列表
docker manifest push username/test用户直接拉取 username/test,Docker 会根据宿主机架构自动选择正确镜像。
(6)测试
在不同架构主机上执行:
docker run -it --rm username/test- Docker 会自动选择 x86_64 或 ARM64 镜像
- 用户无需手动选择架构镜像
调试方法
docker manifest inspect <镜像>查看支持架构docker run --platform <架构>可手动指定