什么是 Dockerfile?

Dockerfile 是一个文本文件,它包含了一系列用户可以调用命令行来自动构建 Docker 镜像的指令。简单来说,Dockerfile 就是构建镜像的“说明书”或“配方”。通过 docker build 命令,Docker 可以读取 Dockerfile 中的指令,并按顺序执行,最终生成一个自定义的 Docker 镜像。

Dockerfile 基本结构和常用指令

一个典型的 Dockerfile 通常包含以下几个部分,并按特定顺序组织:

  1. FROM - 指定基础镜像:

    • 每个 Dockerfile 的第一条非注释指令必须FROM
    • 它指定了你构建新镜像所依赖的基础镜像。例如,如果你要构建一个运行 Python 应用的镜像,可能会选择一个官方的 Python 镜像作为基础。
    • 语法:FROM <image>:<tag>FROM <image>@<digest>
    • 示例:FROM python:3.9-slimFROM ubuntu:22.04
  2. LABEL - 添加元数据 (可选):

    • 用于为镜像添加元数据,如维护者信息、版本号等。
    • 语法:LABEL <key>=<value> <key>=<value> ...
    • 示例:LABEL maintainer="Your Name <your.email@example.com>"
  3. ARG - 定义构建时变量 (可选):

    • 定义在 docker build 命令执行时可以传递给构建过程的变量。注意,ARG 定义的变量在镜像构建完成后通常不可用,除非它被 ENV 指令使用。
    • 语法:ARG <name>[=<default value>]
    • 示例:ARG APP_VERSION=1.0
  4. ENV - 设置环境变量:

    • 设置环境变量。这些变量在镜像构建过程中以及基于该镜像启动的容器中都可用。
    • 语法:ENV <key>=<value>ENV <key1>=<value1> <key2>=<value2> ...
    • 示例:ENV PYTHONDONTWRITEBYTECODE=1 (阻止 Python 生成 .pyc 文件)
    • 示例:ENV APP_HOME=/app
  5. WORKDIR - 设置工作目录:

    • 为后续的 RUN, CMD, ENTRYPOINT, COPY, ADD 指令设置工作目录。如果目录不存在,WORKDIR 会自动创建它。
    • 推荐使用绝对路径。
    • 语法:WORKDIR /path/to/workdir
    • 示例:WORKDIR /app (后续指令将在 /app 目录下执行)
  6. COPY / ADD - 复制文件或目录:

    • COPY: 将构建上下文(通常是 Dockerfile 所在的目录及其子目录)中的文件或目录复制到镜像的文件系统中。
      • 语法:COPY [--chown=<user>:<group>] <src>... <dest>
      • 示例:COPY . . (将当前目录所有内容复制到镜像的当前工作目录)
      • 示例:COPY requirements.txt /app/
    • ADD: 功能与 COPY 类似,但增加了两个额外功能:
      • <src> 可以是 URL。
      • 如果源 <src> 是一个可识别的压缩格式(如 tar, gzip, bzip2, xz),它会被自动解压缩到目标 <dest>
      • 注意: 除非明确需要 ADD 的特殊功能(如解压或下载 URL),否则官方推荐优先使用 COPY,因为它的行为更明确、可预测。
      • 语法:ADD [--chown=<user>:<group>] <src>... <dest>
      • 示例:ADD https://example.com/file.tar.gz /tmp/ (下载并解压)
  7. RUN - 执行构建命令:

    • 在镜像构建过程中执行指定的命令。通常用于安装软件包、创建目录、编译代码等。
    • 每条 RUN 指令都会在当前镜像的顶层创建一个新的层。为了减少镜像层数,通常建议将多个相关的命令合并到一条 RUN 指令中,使用 && 连接。
    • 有两种格式:
      • Shell 格式:RUN <command> (命令在 shell 中执行,通常是 /bin/sh -c)
      • Exec 格式:RUN ["executable", "param1", "param2"] (直接执行,不通过 shell)
    • 示例(Shell 格式):
      1
      2
      3
      4
      RUN apt-get update && apt-get install -y --no-install-recommends \
      package1 \
      package2 \
      && rm -rf /var/lib/apt/lists/*
    • 示例(Exec 格式,较少用于 RUN):
      1
      RUN ["apt-get", "update"]
  8. EXPOSE - 声明端口:

    • 声明容器在运行时打算监听的网络端口。这不会实际发布端口,只是作为文档告诉用户(或自动化工具)哪些端口是有意暴露的。
    • 实际发布端口需要在 docker run 时使用 -p-P 参数。
    • 语法:EXPOSE <port> [<port>/<protocol>...] (默认协议是 TCP)
    • 示例:EXPOSE 8080EXPOSE 80/tcp 53/udp
  9. VOLUME - 定义匿名卷 (可选):

    • 创建一个指定名称的挂载点,并将其标记为持有来自外部挂载的卷或来自其他容器的卷。常用于数据库存储、日志文件等需要持久化或共享的数据。
    • 语法:VOLUME ["/path/to/volume"]
    • 示例:VOLUME ["/var/log", "/data"]
  10. USER - 设置运行用户 (可选):

    • 指定运行后续 RUN, CMD, ENTRYPOINT 指令时使用的用户名或 UID(以及可选的组名或 GID)。
    • 为了安全起见,推荐在不需要 root 权限时切换到非 root 用户。
    • 语法:USER <user>[:<group>]USER <UID>[:<GID>]
    • 示例:RUN useradd -ms /bin/bash myuser
    • 示例:USER myuser
  11. ENTRYPOINT - 配置容器启动时执行的命令:

    • 允许你将容器配置为像可执行文件一样运行。
    • ENTRYPOINT 指定的命令不容易在容器启动时被覆盖(除非使用 docker run --entrypoint)。
    • 通常与 CMD 配合使用,CMD 提供 ENTRYPOINT 的默认参数。
    • 有两种格式:
      • Exec 格式(推荐):ENTRYPOINT ["executable", "param1", "param2"]
      • Shell 格式:ENTRYPOINT command param1 param2 (会在 /bin/sh -c 中执行,可能导致无法接收信号和 PID 1 问题,一般不推荐)
    • 示例(Exec 格式):ENTRYPOINT ["python", "app.py"]
  12. CMD - 提供容器启动的默认命令或参数:

    • CMD 指令有三种用途:
      • ENTRYPOINT 指令提供默认参数。
      • 如果 Dockerfile 中没有 ENTRYPOINT,则 CMD 指定容器启动时要执行的默认命令。
      • 如果在 docker run 命令中指定了其他命令,CMD 的值会被覆盖。
    • 一个 Dockerfile 中只能有一条 CMD 指令生效。如果有多条,只有最后一条会生效。
    • 有三种格式:
      • Exec 格式(推荐,尤其是在没有 ENTRYPOINT 或作为 ENTRYPOINT 参数时):CMD ["executable","param1","param2"]
      • 作为 ENTRYPOINT 的默认参数:CMD ["param1","param2"]
      • Shell 格式:CMD command param1 param2
    • 示例(配合 ENTRYPOINT):
      1
      2
      ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"]
      CMD ["-c", "/etc/nginx/nginx.conf"] # 提供默认配置文件参数
    • 示例(独立使用,提供默认执行命令):
      1
      CMD ["python", "app.py"]
    • 示例(Shell 格式):
      1
      CMD echo "Hello Docker"
  13. .dockerignore 文件:

    • 这不是 Dockerfile 的一部分,但在构建镜像时非常重要。
    • 它是一个位于构建上下文根目录(通常和 Dockerfile 在同一目录)的文件,用于指定在执行 docker build 时哪些文件或目录不应被发送到 Docker 守护进程(即不包含在构建上下文中)。
    • 语法类似于 .gitignore
    • 这有助于减小构建上下文的大小,加快构建速度,并避免将敏感信息或不必要的文件复制到镜像中。
    • 示例 .dockerignore 内容:
      1
      2
      3
      4
      5
      .git
      node_modules
      *.log
      Dockerfile
      .dockerignore

编写 Dockerfile 的最佳实践

  1. 保持镜像尽可能小:

    • 选择合适的基础镜像(如 alpine, slim 版本)。
    • 只安装必要的软件包。
    • 清理不必要的文件,例如在 RUN 指令的最后清理包管理器的缓存(如 apt-get clean, rm -rf /var/lib/apt/lists/*)。
    • 使用多阶段构建(Multi-stage builds)。
  2. 利用构建缓存:

    • Docker 会缓存 RUN, COPY, ADD 等指令的结果层。
    • 将不经常变化的指令(如安装基础依赖)放在 Dockerfile 的前面。
    • 将经常变化的指令(如 COPY 应用程序代码)放在后面。
    • 例如,先 COPY 依赖文件(如 requirements.txtpackage.json)并安装依赖,然后再 COPY 整个应用程序代码。
  3. 合并 RUN 指令:

    • 每条 RUN 指令都会创建一个新的镜像层。过多的层会增加镜像大小。
    • 使用 && 将多个命令连接在一条 RUN 指令中。
  4. 使用 .dockerignore:

    • 排除不需要发送到 Docker 守护进程的文件和目录。
  5. 优先使用 COPY 而不是 ADD:

    • 除非你需要 ADD 的特定功能(URL 下载或自动解压),否则 COPY 更清晰、更可预测。
  6. 使用多阶段构建 (Multi-stage Builds):

    • 对于需要编译步骤的应用程序(如 Go, Java, C++),可以在一个阶段(构建阶段)进行编译和构建,然后在另一个更小的阶段(运行阶段)只复制编译后的产物和必要的运行时依赖。这可以显著减小最终镜像的大小。
    • 示例结构:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      # ---- Build Stage ----
      FROM golang:1.19 AS builder
      WORKDIR /app
      COPY . .
      RUN go build -o myapp .

      # ---- Runtime Stage ----
      FROM alpine:latest
      WORKDIR /app
      COPY --from=builder /app/myapp .
      # Copy other necessary files like configs if needed
      # COPY --from=builder /app/config.yaml .
      CMD ["./myapp"]
  7. 使用 Exec 格式的 CMDENTRYPOINT:

    • 避免使用 Shell 格式,因为它会隐式启动一个 shell 进程 (/bin/sh -c),这可能导致信号处理问题(例如,docker stop 可能无法正常停止应用)并且你的应用不会是 PID 1。
  8. 以非 Root 用户运行:

    • 增加安全性,避免容器内的进程拥有宿主机的 root 权限。使用 USER 指令切换用户。
  9. 明确指定包版本:

    • RUN 指令中安装软件包时,尽可能指定版本号,以确保构建的可重复性。
  10. 添加 LABEL:

    • 提供镜像的元数据,方便管理和理解。

简单示例:一个 Python Flask 应用的 Dockerfile

假设你的项目结构如下:

1
2
3
4
.
├── app.py
├── requirements.txt
└── Dockerfile

requirements.txt:

1
Flask==2.1.2

app.py:

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
return 'Hello, Docker!'

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 1. 使用官方 Python 运行时作为父镜像
FROM python:3.9-slim

# 2. 设置环境变量 (可选,但推荐)
ENV PYTHONDONTWRITEBYTECODE 1 # 阻止 Python 写入 .pyc 文件
ENV PYTHONUNBUFFERED 1 # 让 Python 输出直接发送到终端,方便查看日志

# 3. 设置工作目录
WORKDIR /app

# 4. 复制依赖文件并安装依赖 (利用缓存)
# 只复制 requirements.txt,如果它没有变化,则下面安装的层会被缓存
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 5. 复制项目代码到工作目录
COPY . .

# 6. 声明应用监听的端口
EXPOSE 5000

# 7. (可选) 创建非 root 用户并切换
# RUN useradd -m myuser
# USER myuser

# 8. 定义容器启动时运行的命令
CMD ["python", "app.py"]

如何构建镜像

在包含 Dockerfile 和你的应用程序代码的目录下,打开终端,运行以下命令:

1
2
3
# -t 参数用于给镜像打标签 (name:tag)
# . 表示使用当前目录作为构建上下文
docker build -t my-python-app:latest .

构建成功后,你就可以使用这个镜像来运行容器了:

1
2
# -p 参数将宿主机的 8080 端口映射到容器的 5000 端口
docker run -p 8080:5000 my-python-app:latest

现在,访问 http://localhost:8080 应该就能看到 “Hello, Docker!” 了。