Dockerfile使用与最佳实践

发布于:2025-06-04 ⋅ 阅读:(43) ⋅ 点赞:(0)

一、Dockerfile是什么?

Dockerfile 是 Docker 的核心组件之一,它本质上是一个文本文件,包含了一系列用于自动构建 Docker 镜像的指令。你可以把它想象成一个菜谱,Docker 引擎(docker build 命令)就是这个厨师,它按照菜谱(Dockerfile)一步步执行操作,最终“烹饪”出一个可运行的容器镜像(Docker Image)。

二、核心概念

  1. 基础镜像 (Base Image):通常是 FROM 指令指定的一个现有镜像。你的镜像将在这个基础之上构建。例如 FROM ubuntu:22.04FROM python:3.11-slim

  2. 层 (Layers):Dockerfile 中的每条指令在执行时都会创建一个新的只读层。这些层像堆叠的透明幻灯片一样组成了最终的镜像。这种分层机制是 Docker 轻量化和高效(缓存、共享层)的关键。

  3. 构建上下文 (Build Context):当你运行 docker build 时,当前目录(或你指定的路径)及其子目录会被打包发送给 Docker 守护进程。Dockerfile 中的指令(如 COPY, ADD)只能操作这个上下文中的文件。注意:.dockerignore文件可以排除不需要发送到上下文的文件(类似于 .gitignore),提高构建速度和安全性。

三、常用 Dockerfile 指令详解

  1. FROM:
  • 作用: 指定构建新镜像所基于的基础镜像。必须是 Dockerfile 的第一条有效指令(注释除外)。
  • 语法: FROM <image>[:<tag>] [AS <name>]
  • 示例:
FROM ubuntu:22.04
FROM python:3.11-slim-bullseye AS build-stage
  1. 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 形式
  1. 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用户)
  1. ADD
  • 作用:功能类似 COPY,但增加了:自动解压本地 .tar, .tar.gz, .tar.bz2, .tar.xz 文件到目标路径。
    可以从 URL 下载文件并复制到镜像(不推荐,因为下载的文件不会进入缓存层,且不如用 RUN curl/wget 灵活)。

  • 建议: 优先使用 COPY,除非明确需要自动解压功能。ADD 的行为有时不够透明。

  1. WORKDIR
  • 作用: 为后续的 RUN, CMD, ENTRYPOINT, COPY, ADD 指令设置工作目录。如果目录不存在,会自动创建。强烈推荐使用,避免使用 RUN cd ... && do something 这种容易出错的方式。

  • 语法: WORKDIR /path/to/workdir

  • 示例:

WORKDIR /app
COPY . .          # 现在复制到 /app
RUN python setup.py install
  1. 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 形式
  1. 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`
  1. EXPOSE:
  • 作用: 声明容器在运行时将监听哪些网络端口。这只是一个文档说明和元数据设置,并不会实际打开端口! 实际端口映射需要在 docker run 时用 -p/--publish 参数指定。

  • 语法: EXPOSE <port> [<port>/<protocol>...] (协议默认为 TCP)

  • 示例: EXPOSE 80/tcp 443/tcp

  1. ENV:
  • 作用: 设置环境变量,这些变量在构建过程和最终运行的容器中都可用。后续指令和容器内的进程都可以使用它们。

  • 语法:
    ENV <key>=<value> ... (一次设置多个变量,推荐)
    ENV <key> <value> (旧式,一次设置一个变量)

  • 示例:

ENV APP_HOME=/app \
    PYTHONUNBUFFERED=1 \
    NODE_ENV=production
  1. 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 .  # 注意最后有一个点
  1. USER:
  • 作用: 指定后续指令 (RUN, CMD, ENTRYPOINT) 以及容器运行时的默认用户名或 UID。强烈推荐在运行时使用非 root 用户以增强安全性

  • 语法: USER [:] 或 USER [:]

  • 示例:

RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
CMD ["python", "app.py"]
  1. VOLUME:
  • 作用: 在镜像中创建一个挂载点,用于将宿主机或其他容器的目录/卷挂载到容器中。即使容器被删除,挂载到该点的数据也会保留(在宿主机卷或命名卷中)。主要用于声明容器需要持久化或共享数据的目录。

  • 语法: VOLUME ["/path/to/dir1", "/path/to/dir2", ...]VOLUME /path/to/dir

  • 示例: VOLUME /var/lib/mysql (声明 MySQL 数据存储目录)

  1. .dockerignore:
  • 作用: 虽然不是 Dockerfile 里的指令,但它与构建过程紧密相关。它位于构建上下文根目录,用于指定在 docker build 发送上下文给守护进程时需要忽略的文件和目录模式(类似于 .gitignore)。强烈推荐使用以减小上下文大小、加快构建速度并避免将敏感文件(如 .env, *.pem)意外复制到镜像中。

  • 示例 .dockerignore:

.git
.DS_Store
*.log
Dockerfile*
docker-compose*
node_modules
venv
.env
*.pem

四、编写高效 Dockerfile 的最佳实践

  1. 选择合适的基础镜像: 优先选择官方镜像、体积小的变体(如 -slim, -alpine)。避免不必要的软件包。

  2. 利用构建缓存: Docker 按顺序执行指令,并将结果缓存为层。如果某层及其之前的层没有变化,Docker 会直接使用缓存。把最不常变动的指令(如安装依赖)放在前面,把最常变动的指令(如复制应用代码)放在后面。

  3. 合并 RUN 指令: 使用 && 和 \ 将多个相关命令(如 apt-get update && apt-get install -y ...)合并到一个 RUN 指令中,减少不必要的层数。清理缓存(如 rm -rf /var/lib/apt/lists/*)也应放在同一个 RUN 中。

  4. 谨慎使用 COPY/ADD: 明确指定需要复制的文件,避免使用宽泛的 COPY . .。利用 .dockerignore

  5. *明确指定用户 (USER): 不要在容器内以 root 用户运行应用,除非绝对必要。

  6. 优先使用 Exec 形式: 对于 CMD ENTRYPOINT,优先使用 Exec 形式 (["executable", "arg1", "arg2"])。这能确保可执行程序是 PID 1,能正确接收 Unix 信号。

  7. 合理使用 ENTRYPOINT 和 CMD: 理解它们的交互关系。通常 ENTRYPOINT 定义主程序,CMD 定义默认参数。

  8. 减少层大小: 每个指令都会增加层大小。删除临时文件、清理缓存应在同一层中进行。

  9. 标记镜像 (LABEL): 使用 LABEL 指令添加元数据(维护者、版本、描述等),方便管理。

  10. 多阶段构建 (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系统

  1. 需求说明:创建一个增强版自带 vim\ifconfig\java17环境的centos系统;
  2. 编写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

  1. 镜像构建
# 注意: tag 后面有一个空格,空格后面有一个点,tag 尽量明确,不推荐使用latest,容易产生虚悬镜像
docker build -t 新镜像的名称:TAG .   

六、 虚悬镜像(Dangling Images)

虚悬镜像是 Docker 中一个常见但容易被忽视的概念,理解它对优化 Docker 环境至关重要。

什么是虚悬镜像?

仓库名、标签都是<none>的镜像,俗称dangling image
它们通常表现为:

  • docker images 列表中显示为 <none>:<none>
  • REPOSITORYTAG 列都显示为 <none>

虚悬镜像的产生原因

  1. 镜像重建(最常见原因):

    • 当使用相同标签重新构建镜像时,旧镜像会失去标签

      例如:docker build -t myapp:latest . 多次执行

  2. 拉取新版本:

    • 当拉取镜像的新版本时(如 docker pull nginx:latest

    • 旧版本的 nginx:latest 会变成虚悬镜像

  3. 手动删除标签:

docker rmi myapp:old-version
  1. 多阶段构建:

    • 构建过程中的中间层可能成为虚悬镜像

识别虚悬镜像
使用以下命令查看虚悬镜像:

# 列出所有虚悬镜像
docker images -f "dangling=true"

# 简洁输出(仅显示ID)
docker images -f "dangling=true" -q

虚悬镜像的影响

  1. 磁盘空间:占用大量存储空间,尤其是大型应用;
  2. 性能:可能影响镜像拉取和容器启动速度;
  3. 管理混乱: 使镜像列表难以阅读和维护;
  4. 安全风险: 可能包含未更新的安全漏洞;

管理虚悬镜像

  1. 安全删除虚悬镜像
# 删除所有虚悬镜像
docker image prune

# 强制删除(无确认提示)
docker image prune -f

# 删除所有未被使用的镜像(包括虚悬镜像)
docker image prune -a
  1. 删除特定虚悬镜像
# 先列出所有虚悬镜像
docker images -f "dangling=true"

# 然后删除指定ID的镜像
docker rmi d123456789ab
  1. 自动清理策略
    在 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

网站公告

今日签到

点亮在社区的每一天
去签到