我们来系统且深入地讲清楚 Dockerfile 中最常用的命令。
Dockerfile 是一个文本文档,它包含了用户在命令行上可以调用来组合成一个镜像的所有命令。它本质上是构建 Docker 镜像的“蓝图”或“食谱”。理解它的核心命令是高效、安全地使用 Docker 的第一步。
我将按照一个典型 Dockerfile 的书写顺序和逻辑关系来展开讲解。
1. 基础与起点 (Foundation)
FROM
这是每一个 Dockerfile 的第一条指令(除了少数以 ARG 开头的特殊情况)。
-
核心作用:指定新镜像所基于的基础镜像 (Base Image)。镜像是分层的,
FROM定义了你的镜像的起始层。 -
语法:
Dockerfile
FROM <image>[:<tag>] [AS <name>] -
详解:
-
<image>:<tag>:例如ubuntu:22.04、python:3.11-slim、nginx:latest。选择一个尽可能小且满足需求的基础镜像是优化的关键。slim、alpine这类标签通常表示轻量级版本。 -
AS <name>:这是在多阶段构建 (Multi-stage builds) 中为该构建阶段命名,后面会详细讲。
-
-
示例:
Dockerfile
# 使用官方的 Node.js 18 的 alpine 精简版作为基础镜像 FROM node:18-alpine
2. 构建指令 (Build Instructions)
这部分命令在 docker build 过程中执行,用于向镜像中添加文件、安装软件和进行配置。
RUN
构建镜像时执行的命令。
-
核心作用:安装软件包、创建目录、编译代码等。每条
RUN指令都会在当前镜像的顶层创建一个新的层 (Layer)。 -
语法:
-
Shell 格式:
RUN <command>(命令在/bin/sh -c中执行) -
Exec 格式:
RUN ["executable", "param1", "param2"](直接执行,不经过 shell)
-
-
详解与最佳实践:
-
合并命令:为了减少镜像的层数和大小,应该将多个相关的
RUN命令用&&连接起来。例如,更新包列表和安装软件应该放在一条RUN指令里。 -
清理缓存:在同一条
RUN指令中,安装完软件包后应立即清理缓存文件,以减小镜像体积。
-
-
示例:
Dockerfile
# 不好的例子:创建了两个不必要的层 # RUN apt-get update # RUN apt-get install -y curl # 好的例子:合并为一层,并清理缓存 RUN apt-get update && \ apt-get install -y curl \ && rm -rf /var/lib/apt/lists/*
COPY 与 ADD
将本地文件或目录复制到镜像的文件系统中。
-
核心作用:将应用程序代码、配置文件等添加到镜像中。
-
语法:
Dockerfile
COPY [--chown=<user>:<group>] <src>... <dest> ADD [--chown=<user>:<group>] <src>... <dest> -
详解与抉择:
-
COPY:功能直接纯粹,就是从构建上下文 (build context) 中复制文件到镜像。 -
ADD:比COPY多了两个额外功能:-
源文件
<src>可以是一个 URL。 -
如果源文件是一个本地的 tar 压缩包(如
.tar.gz),ADD会自动解压它。
-
-
最佳实践与我的选择:
始终优先使用 COPY。它的行为更可预测、更透明。ADD 的自动解压和远程 URL 功能会带来不可预见的风险(例如,URL 内容变化、解压出非预期的文件结构),并且可能导致缓存失效。如果需要下载文件,应该使用 RUN wget ... 或 RUN curl ...,这样可以更好地控制下载过程和进行清理。如果需要解压,使用 RUN tar -xvf ...。
-
-
示例:
Dockerfile
# 复制当前目录下的 package.json 和 package-lock.json 到镜像的 /app/ 目录下 COPY package*.json ./app/ # 复制整个项目到镜像的 /app/ 目录下 COPY . ./app/优化提示:先复制
package.json并执行npm install,再复制整个项目代码。这样,如果你的项目代码变了,但依赖没变,Docker 可以利用缓存,跳过npm install这一耗时步骤。
WORKDIR
设置工作目录。
-
核心作用:为后续的
RUN,CMD,ENTRYPOINT,COPY,ADD指令设置当前工作目录。如果目录不存在,WORKDIR会自动创建它。 -
语法:
WORKDIR /path/to/workdir -
详解:
- 使用
WORKDIR而不是RUN cd /path。WORKDIR的效果是持久的,会影响之后的所有指令。而RUN cd只在当前那条RUN指令中有效。
- 使用
-
示例:
Dockerfile
WORKDIR /app # 下面的 COPY 和 RUN 都在 /app 目录下执行 COPY package*.json . RUN npm install COPY . .
3. 运行时配置 (Runtime Configuration)
这部分命令不影响镜像的构建,但会影响基于此镜像启动的容器的行为。
CMD 与 ENTRYPOINT
这两个命令都用于指定容器启动时要执行的命令,但它们的侧重点和交互方式不同,是 Dockerfile 中最容易混淆的命令。
-
核心作用:定义容器的默认执行命令。
-
详解与对比:
| 特性 | CMD (命令) |
ENTRYPOINT (入口点) |
|---|---|---|
| 目的 | 为容器提供一个默认的、可被覆盖的执行命令。 | 将容器配置为像一个可执行文件,提供固定的入口。 |
| 覆盖方式 | 在 docker run 时附加的命令会完全覆盖 CMD 的内容。 |
在 docker run 时附加的命令会作为参数追加给 ENTRYPOINT。 |
| 语法 (推荐) | Exec 格式: CMD ["executable", "param1", "param2"] |
Exec 格式: ENTRYPOINT ["executable", "param1", "param2"] |
| 语法 (不推荐) | Shell 格式: CMD command param1 param2 |
Shell 格式: ENTRYPOINT command param1 param2 |
-
如何选择和组合使用?
-
只想运行一个命令,且希望用户可以轻松替换它:
-
使用
CMD。 -
场景:基础镜像如
ubuntu,默认启动bash,但用户可以docker run ubuntu ls -l来执行不同命令。 -
示例:
CMD ["python", "app.py"]。用户可以通过docker run my-app python manage.py shell来覆盖。
-
-
想让容器表现得像一个固定的程序,只接受参数:
-
使用
ENTRYPOINT。 -
场景:创建一个只用于
ping的工具容器。 -
示例:
ENTRYPOINT ["ping"]。用户运行docker run my-ping-container google.com,实际执行的是ping google.com。
-
-
最佳实践:
ENTRYPOINT+CMD组合-
这是最强大和灵活的用法。
ENTRYPOINT定义主命令,CMD为主命令提供默认参数。 -
场景:一个 Web 服务,主命令是固定的,但可以调整启动参数。
-
示例:
Dockerfile
ENTRYPOINT ["java", "-jar", "/app/app.jar"] CMD ["--spring.profiles.active=prod"]-
用户直接
docker run my-app,执行的是java -jar /app/app.jar --spring.profiles.active=prod。 -
用户
docker run my-app --spring.profiles.active=dev,CMD的默认参数被覆盖,执行的是java -jar /app/app.jar --spring.profiles.active=dev。
-
-
-
EXPOSE
声明容器在运行时监听的网络端口。
-
核心作用:这是一个文档性的指令,告诉使用者这个镜像中的服务倾向于使用哪个端口。它不会自动将端口发布到主机上。
-
语法:
EXPOSE <port> [<port>/<protocol>...] -
详解:
-
它只是元数据,实际的端口映射需要在
docker run时通过-p或-P参数来完成。 -
例如
docker run -p 8080:80 my-image,将主机的 8080 端口映射到容器的 80 端口。EXPOSE 80只是提示你容器内服务在 80 端口。
-
-
示例:
Dockerfile
# 声明容器内的 Nginx 服务监听 80 端口 EXPOSE 80
ENV
设置环境变量。
-
核心作用:定义环境变量,这些变量在镜像的构建过程 (
RUN指令) 和容器的运行过程中都可用。 -
语法:
ENV <key>=<value> ... -
详解:
- 对于配置信息,如数据库地址、API 密钥等,使用环境变量是一种非常好的实践,因为它将配置与代码分离。
-
示例:
Dockerfile
# 设置一个版本号,方便后续指令使用 ENV APP_VERSION=1.0 # 设置应用配置 ENV DB_HOST=database ENV DB_PORT=5432
4. 元数据与高级指令 (Metadata & Advanced)
ARG
定义构建时变量。
-
核心作用:
ARG定义的变量只在docker build过程中有效,容器运行时不可用。 -
语法:
ARG <name>[=<default value>] -
与
ENV的区别:-
ARG是构建时 (build-time) 的。 -
ENV是构建时和运行时 (run-time) 都有。
-
-
用法:
-
可以在
docker build命令中通过--build-arg来传递值。 -
ARG可以放在FROM之前,用于动态指定基础镜像。
-
-
示例:
Dockerfile
ARG PYTHON_VERSION=3.11 FROM python:${PYTHON_VERSION}-slim ARG APP_DIR=/app WORKDIR ${APP_DIR}构建时可以这样覆盖默认值:
docker build --build-arg PYTHON_VERSION=3.10 .
USER
设置运行容器时的用户名或 UID。
-
核心作用:出于安全考虑,避免以
root用户身份运行容器内的应用。 -
语法:
USER <user>[:<group>] -
最佳实践:
- 在
RUN指令中创建非 root 用户,然后在 Dockerfile 的末尾使用USER指令切换。
- 在
-
示例:
Dockerfile
# 创建一个非 root 用户 'appuser' RUN addgroup -S appgroup && adduser -S appuser -G appgroup # 切换到这个用户 USER appuser
VOLUME
创建一个可以挂载的卷。
-
核心作用:指定镜像中的某个目录为数据卷。当容器启动时,Docker 会将这个目录的内容管理起来,用于持久化数据,或者在容器间共享数据。
-
语法:
VOLUME ["/path/to/volume"] -
详解:
-
这个指令可以帮助防止容器内的应用向容器的可写层写入大量数据,从而保持镜像的整洁。
-
通常用于数据库的数据目录、应用的日志目录等。
-
-
示例:
Dockerfile
# 将 /var/log 目录声明为数据卷,容器内产生的日志将写入到这个卷中 VOLUME /var/log
总结与完整示例
下面是一个注释详尽的 Node.js 应用的 Dockerfile,它运用了多阶段构建等最佳实践。
Dockerfile
# ==================================
# 第一阶段:构建 (Builder Stage)
# ==================================
# 使用一个包含完整构建工具链的镜像作为构建环境
FROM node:18 AS builder
# 设置构建阶段的工作目录
WORKDIR /app
# 复制 package.json 和 lock 文件
# 这样可以利用 Docker 缓存,只有在依赖变化时才重新安装
COPY package*.json ./
# 安装所有依赖,包括开发依赖
RUN npm install
# 复制所有源代码到工作目录
COPY . .
# 执行构建命令 (例如,编译 TypeScript, 打包前端资源)
RUN npm run build
# ==================================
# 第二阶段:生产 (Production Stage)
# ==================================
# 使用一个非常轻量的基础镜像用于生产环境
FROM node:18-alpine
# 设置生产环境的工作目录
WORKDIR /app
# 从'builder'阶段复制 package.json 和 lock 文件
COPY package*.json ./
# 只安装生产环境所需的依赖,减小最终镜像大小
RUN npm install --omit=dev
# 从'builder'阶段复制构建好的应用文件
# 这就是多阶段构建的核心:只把最终产物拿过来
COPY --from=builder /app/dist ./dist
# 创建一个非 root 用户来运行应用,增强安全性
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# 对外暴露应用程序的端口
EXPOSE 3000
# 容器启动时执行的默认命令
CMD ["node", "dist/main.js"]
这个例子展示了如何组合使用这些命令来创建一个优化、安全且高效的 Docker 镜像。掌握这些核心命令及其背后的原理,你就能自如地为任何应用编写高质量的 Dockerfile。