一、Dockerfile是什么?
Dockerfile 是 Docker 的核心组件之一,它本质上是一个文本文件,包含了一系列用于自动构建 Docker 镜像的指令。你可以把它想象成一个菜谱,Docker 引擎(docker build 命令)就是这个厨师,它按照菜谱(Dockerfile)一步步执行操作,最终“烹饪”出一个可运行的容器镜像(Docker Image)。
二、核心概念
基础镜像 (Base Image):通常是
FROM
指令指定的一个现有镜像。你的镜像将在这个基础之上构建。例如FROM ubuntu:22.04
或FROM python:3.11-slim
。层 (Layers):Dockerfile 中的每条指令在执行时都会创建一个新的只读层。这些层像堆叠的透明幻灯片一样组成了最终的镜像。这种分层机制是 Docker 轻量化和高效(缓存、共享层)的关键。
构建上下文 (Build Context):当你运行
docker build
时,当前目录(或你指定的路径)及其子目录会被打包发送给 Docker 守护进程。Dockerfile 中的指令(如COPY, ADD
)只能操作这个上下文中的文件。注意:.dockerignore
文件可以排除不需要发送到上下文的文件(类似于.gitignore
),提高构建速度和安全性。
三、常用 Dockerfile 指令详解
- FROM:
- 作用: 指定构建新镜像所基于的基础镜像。必须是 Dockerfile 的第一条有效指令(注释除外)。
- 语法:
FROM <image>[:<tag>] [AS <name>]
- 示例:
FROM ubuntu:22.04
FROM python:3.11-slim-bullseye AS build-stage
- RUN:
作用:在构建镜像的过程中,在当前层执行 shell 命令。常用于安装软件包、编译代码、创建目录等。
语法:
Shell 形式:RUN <command>
(默认在 /bin/sh -c 下执行)
Exec 形式:RUN ["executable", "param1", "param2"]
(直接执行,避免 shell 处理)示例:
RUN apt-get update && apt-get install -y curl wget # Shell 形式 (推荐合并命令减少层数)
RUN ["/bin/bash", "-c", "echo 'Hello from exec form'"] # Exec 形式
- COPY:
作用: 将构建上下文中的文件或目录复制到镜像内部的指定路径。
语法:
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>", ... "<dest>"]
(路径包含空格时使用)示例:
COPY requirements.txt /app/ # 复制单个文件
COPY . /app/ # 复制当前上下文所有内容到 /app (谨慎使用!)
COPY --chown=node:node package*.json ./ # 复制并设置文件所有者 (常用于非root用户)
- ADD:
作用:功能类似
COPY
,但增加了:自动解压本地 .tar, .tar.gz, .tar.bz2, .tar.xz 文件到目标路径。
可以从 URL 下载文件并复制到镜像(不推荐,因为下载的文件不会进入缓存层,且不如用 RUN curl/wget 灵活)。建议: 优先使用
COPY
,除非明确需要自动解压功能。ADD
的行为有时不够透明。
- WORKDIR:
作用: 为后续的
RUN, CMD, ENTRYPOINT, COPY, ADD
指令设置工作目录。如果目录不存在,会自动创建。强烈推荐使用
,避免使用RUN cd ... && do something
这种容易出错的方式。语法: WORKDIR /path/to/workdir
示例:
WORKDIR /app
COPY . . # 现在复制到 /app
RUN python setup.py install
- CMD:
作用:指定容器启动时默认执行的命令。一个 Dockerfile 中只能有一个有效的 CMD(
如果写了多个,只有最后一个生效
)。主要目的是为容器提供默认的执行行为。语法:
Exec 形式 (推荐):CMD ["executable", "param1", "param2"]
Shell 形式:CMD command param1 param2
作为ENTRYPOINT
的参数:CMD ["param1", "param2"]
(需结合 ENTRYPOINT 的 Exec 形式使用)
关键点:
docker run 会覆盖 Dockerfile 中的 CMD。
如果 CMD 是 shell 形式,命令会在 /bin/sh -c 下执行。示例:
CMD ["python", "app.py"] # Exec 形式 (推荐)
CMD python app.py # Shell 形式
- ENTRYPOINT:
作用: 配置容器启动时运行的可执行程序。让容器像一个可执行文件一样运行。通常用于定义容器的主要目的。
语法:
Exec 形式 (推荐): ENTRYPOINT ["executable", "param1", "param2"]
Shell 形式: ENTRYPOINT command param1 param2 (不推荐,会忽略 CMD 和 docker run 参数)
与 CMD 的关系:
当 ENTRYPOINT 是 Exec 形式时:CMD 的内容会作为参数传递给 ENTRYPOINT。
docker run <image> <args>
中的 会覆盖 CMD 的内容,然后作为参数传递给 ENTRYPOINT。
当 ENTRYPOINT 是 Shell 形式时:
它会忽略 CMD 和 docker run 传递的命令行参数。
命令在 /bin/sh -c 下执行,导致可执行程序不是 PID 1,可能无法正确处理信号。示例 (常见模式):
ENTRYPOINT ["/usr/bin/dumb-init", "--"] # 使用 init 进程包装
CMD ["python", "app.py"]
# 运行 `docker run my-image` 相当于执行 `/usr/bin/dumb-init -- python app.py`
# 运行 `docker run my-image bash` 相当于执行 `/usr/bin/dumb-init -- bash`
- EXPOSE:
作用: 声明容器在运行时将监听哪些网络端口。这只是一个文档说明和元数据设置,并不会实际打开端口! 实际端口映射需要在
docker run 时用 -p/--publish
参数指定。语法:
EXPOSE <port> [<port>/<protocol>...]
(协议默认为 TCP)示例:
EXPOSE 80/tcp 443/tcp
- ENV:
作用: 设置环境变量,这些变量在构建过程和最终运行的容器中都可用。后续指令和容器内的进程都可以使用它们。
语法:
ENV <key>=<value> ...
(一次设置多个变量,推荐)
ENV <key> <value>
(旧式,一次设置一个变量)示例:
ENV APP_HOME=/app \
PYTHONUNBUFFERED=1 \
NODE_ENV=production
- ARG:
作用: 定义在构建镜像时可以传递的变量。这些变量只在
docker build
过程中有效,不会存在于最终构建好的镜像中或运行的容器里(除非在构建过程中用 ENV 再次定义)。语法:
ARG <name>[=<default value>]
使用: 通过
docker build --build-arg <varname>=<value>
传递值。示例:
ARG VERSION=latest
FROM base-image:${VERSION}
docker build --build-arg VERSION=2.1 -t myapp:2.1 . # 注意最后有一个点
- USER:
作用: 指定后续指令 (RUN, CMD, ENTRYPOINT) 以及容器运行时的默认用户名或 UID。
强烈推荐在运行时使用非 root 用户以增强安全性
。语法: USER [:] 或 USER [:]
示例:
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
CMD ["python", "app.py"]
- VOLUME:
作用: 在镜像中创建一个挂载点,用于将宿主机或其他容器的目录/卷挂载到容器中。即使容器被删除,挂载到该点的数据也会保留(在宿主机卷或命名卷中)。主要用于声明容器需要持久化或共享数据的目录。
语法:
VOLUME ["/path/to/dir1", "/path/to/dir2", ...]
或VOLUME /path/to/dir
示例:
VOLUME /var/lib/mysql
(声明 MySQL 数据存储目录)
- .dockerignore:
作用: 虽然不是 Dockerfile 里的指令,但它与构建过程紧密相关。它位于构建上下文根目录,用于指定在 docker build 发送上下文给守护进程时
需要忽略
的文件和目录模式(类似于 .gitignore)。强烈推荐使用
以减小上下文大小、加快构建速度并避免将敏感文件(如 .env, *.pem)意外复制到镜像中。示例
.dockerignore:
.git
.DS_Store
*.log
Dockerfile*
docker-compose*
node_modules
venv
.env
*.pem
四、编写高效 Dockerfile 的最佳实践
选择合适的基础镜像: 优先选择官方镜像、体积小的变体(如 -slim, -alpine)。避免不必要的软件包。
利用构建缓存: Docker 按顺序执行指令,并将结果缓存为层。如果某层及其之前的层没有变化,Docker 会直接使用缓存。把最不常变动的指令(如安装依赖)放在前面,把最常变动的指令(如复制应用代码)放在后面。
合并 RUN 指令: 使用 && 和 \ 将多个相关命令(如
apt-get update && apt-get install -y ...
)合并到一个 RUN 指令中,减少不必要的层数。清理缓存(如rm -rf /var/lib/apt/lists/*
)也应放在同一个 RUN 中。谨慎使用 COPY/ADD: 明确指定需要复制的文件,避免使用宽泛的
COPY . .
。利用.dockerignore
。*明确指定用户 (USER): 不要在容器内以 root 用户运行应用,除非绝对必要。
优先使用 Exec 形式: 对于
CMD
和ENTRYPOINT
,优先使用 Exec 形式 (["executable", "arg1", "arg2"]
)。这能确保可执行程序是 PID 1,能正确接收 Unix 信号。合理使用 ENTRYPOINT 和 CMD: 理解它们的交互关系。通常
ENTRYPOINT
定义主程序,CMD
定义默认参数。减少层大小: 每个指令都会增加层大小。删除临时文件、清理缓存应在同一层中进行。
标记镜像 (LABEL): 使用
LABEL
指令添加元数据(维护者、版本、描述等),方便管理。多阶段构建 (Multi-stage builds): 对于需要编译的应用(如 Java, Go, C++),使用多阶段构建。在一个阶段编译和构建,在另一个只包含运行时必要组件的阶段复制构建产物。这能极大减小最终镜像体积。
# 阶段1:构建阶段
FROM maven:3.8.6-openjdk-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# 阶段2:运行阶段
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=builder /build/target/myapp-*.jar /app/myapp.jar
EXPOSE 8080
CMD ["java", "-jar", "/app/myapp.jar"]
五、案例-自定义自己的centos系统
- 需求说明:创建一个增强版自带
vim\ifconfig\java17
环境的centos系统; - 编写Dockerfile文件
# 选择基础镜像
FROM centos:centos7.9.2009
# 镜像维护者的姓名和邮箱
MAINTAINER developer<developer@developer.com>
# 定义环境
ENV MY_PATH /usr/local
# 交互式登录后,默认登录到此目录下
WORKDIR $MY_PATH
# 安装vim 编辑器
RUN yum -y install vim
# 安装ifconfig命令查看网络IP
RUN yum -y install net-tools
# 安装java17及lib库
RUN yum -y install glibc.i686
RUN mkdir /usr/local/java
# ADD 是相对路径,将java17添加到容器中,安装包必须和Dockerfile文件在同一个目录下
ADD jdk-17.0.8_linux-x64_bin.tar.gz /usr/local/java
#配置java环境变量
ENV JAVA_HOME /usr/local/iava/jdk17.0.8
ENV JRE_HOME $JAVA_HOME/Jre
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar: $JRE_HOME/Lib: $CLASSPATH
ENV PATH $JAVA_HOME/bin:$PATH
EXPOSE 80
CMD echo $MYPATH
CMD echo "----- success --------"
CMD /bin/bash
- 镜像构建
# 注意: tag 后面有一个空格,空格后面有一个点,tag 尽量明确,不推荐使用latest,容易产生虚悬镜像
docker build -t 新镜像的名称:TAG .
六、 虚悬镜像(Dangling Images)
虚悬镜像是 Docker 中一个常见但容易被忽视的概念,理解它对优化 Docker 环境至关重要。
什么是虚悬镜像?
仓库名、标签都是
<none>
的镜像,俗称dangling image
它们通常表现为:
- 在
docker images
列表中显示为<none>:<none>
REPOSITORY
和TAG
列都显示为<none>
虚悬镜像的产生原因
镜像重建(最常见原因):
当使用相同标签重新构建镜像时,旧镜像会失去标签
例如:
docker build -t myapp:latest .
多次执行
拉取新版本:
当拉取镜像的新版本时(如
docker pull nginx:latest
)旧版本的 nginx:latest 会变成虚悬镜像
手动删除标签:
docker rmi myapp:old-version
多阶段构建:
- 构建过程中的中间层可能成为虚悬镜像
识别虚悬镜像
使用以下命令查看虚悬镜像:
# 列出所有虚悬镜像
docker images -f "dangling=true"
# 简洁输出(仅显示ID)
docker images -f "dangling=true" -q
虚悬镜像的影响
- 磁盘空间:占用大量存储空间,尤其是大型应用;
- 性能:可能影响镜像拉取和容器启动速度;
- 管理混乱: 使镜像列表难以阅读和维护;
- 安全风险: 可能包含未更新的安全漏洞;
管理虚悬镜像
- 安全删除虚悬镜像
# 删除所有虚悬镜像
docker image prune
# 强制删除(无确认提示)
docker image prune -f
# 删除所有未被使用的镜像(包括虚悬镜像)
docker image prune -a
- 删除特定虚悬镜像
# 先列出所有虚悬镜像
docker images -f "dangling=true"
# 然后删除指定ID的镜像
docker rmi d123456789ab
- 自动清理策略
在 Docker 配置中启用自动清理(/etc/docker/daemon.json):
{
"features": {
"buildkit": true
},
"builder": {
"gc": {
"enabled": true,
"defaultKeepStorage": "20GB"
}
}
}
虚悬镜像 vs. 未使用镜像
特性 | 虚悬镜像 | 未使用镜像 |
---|---|---|
标签 | 无标签 | 可能有标签 |
引用 | 不被任何容器引用 | 不被任何容器引用 |
显示 | <none>:<none> |
正常REPO:TAG |
清理命令 | docker image prune |
docker image prune -a |