Docker 系列 | 04 - 构建你的专属应用:Dockerfile 深度解析

引言

在之前的文章中,我们了解了 Docker 镜像和容器的基本概念,并掌握了如何运行和管理容器。但是,我们如何将自己的应用程序打包成一个可用的 Docker 镜像呢?答案就是 Dockerfile

Dockerfile 是一个简单的文本文件,其中包含了一系列指令,Docker 引擎会按照这些指令一步步地构建镜像。它是实现“一次构建,处处运行”理念的关键,也是容器化应用程序的标准方式。

本篇文章将详细解析 Dockerfile 的常用指令,并通过示例带您亲手构建一个定制化的 Docker 镜像。

什么是 Dockerfile?

Dockerfile 是一个用于自动化构建 Docker 镜像的脚本文件。它包含了一系列指令,每条指令都对应着镜像构建过程中的一个操作,并会创建一个新的镜像层。

通过 Dockerfile,您可以:

  • 定义基础镜像:您的应用将运行在哪个操作系统或基础环境之上。
  • 添加文件:将您的应用程序代码、配置文件等拷贝到镜像中。
  • 安装依赖:安装应用程序所需的运行时、库和工具。
  • 暴露端口:声明容器将对外开放哪些端口。
  • 定义启动命令:指定容器启动时执行的默认命令。

Dockerfile 常用指令详解

以下是 Dockerfile 中一些最常用和最重要的指令:

1. FROM

  • 作用:指定新镜像的基础镜像。Dockerfile 的第一条指令必须是 FROM
  • 语法FROM <image>[:<tag>]
  • 示例
    1
    2
    FROM ubuntu:22.04  # 基于 Ubuntu 22.04 LTS
    FROM python:3.9-slim-buster # 基于 Python 3.9 的轻量级 Debian Buster

2. RUN

  • 作用:在镜像构建过程中执行命令,并将结果作为新层提交。适用于安装软件包、创建文件、执行脚本等。
  • 语法
    • RUN <command> (shell 格式,命令在 /bin/sh -c 中运行)
    • RUN ["executable", "param1", "param2"] (exec 格式,直接执行可执行文件,不会经过 shell 处理)
  • 示例
    1
    2
    RUN apt-get update && apt-get install -y nginx  # 安装 Nginx
    RUN ["pip", "install", "flask"] # 安装 Python 包
    最佳实践:多条 RUN 指令会创建多个层。为了减少镜像层数和镜像大小,通常会将相关的 RUN 指令通过 && 连接起来,并在最后清理不必要的文件(如 apt-get clean 或删除缓存)。

3. COPY

  • 作用:将构建上下文(通常是 Dockerfile 所在的目录)中的文件或目录拷贝到镜像中。
  • 语法COPY <src>... <dest>
  • 示例
    1
    2
    COPY . /app        # 将当前目录所有内容拷贝到镜像的 /app 目录
    COPY requirements.txt /tmp/ # 拷贝单个文件

4. ADD

  • 作用:与 COPY 类似,但 ADD 具有额外功能:
    1. 如果 <src> 是一个 URL,它会下载文件。
    2. 如果 <src> 是一个压缩包(如 .tar, .gz),它会自动解压到 <dest>
  • 语法ADD <src>... <dest>
  • 最佳实践:通常建议优先使用 COPY,因为它的行为更可预测。只有在需要自动解压或从 URL 下载时才使用 ADD

5. WORKDIR

  • 作用:设置 RUN, CMD, ENTRYPOINTCOPY/ADD 指令的工作目录。如果目录不存在,WORKDIR 会自动创建它。
  • 语法WORKDIR /path/to/workdir
  • 示例
    1
    2
    WORKDIR /app # 将 /app 设为后续指令的工作目录
    COPY . . # 现在表示拷贝到 /app 目录下
    最佳实践:在 COPY / ADD 之前设置 WORKDIR 可以简化路径。

6. EXPOSE

  • 作用:声明容器将监听哪些端口。这仅仅是文档性质的声明,并不会实际发布端口。真正的端口映射需要在 docker run 命令中使用 -pdocker-compose.yml 中配置。
  • 语法EXPOSE <port> [<port>...]
  • 示例
    1
    2
    EXPOSE 80    # 声明容器监听 80 端口
    EXPOSE 80 443 # 声明监听 80 和 443 端口

7. CMD

  • 作用:为容器提供默认的执行命令。如果 docker run 命令指定了其他命令,则 CMD 将被忽略。一个 Dockerfile 中只能有一条 CMD 指令,如果有多条,只有最后一条生效。
  • 语法
    • CMD ["executable","param1","param2"] (exec 格式,推荐)
    • CMD command param1 param2 (shell 格式)
    • CMD ["param1","param2"] (作为 ENTRYPOINT 的默认参数)
  • 示例
    1
    2
    CMD ["nginx", "-g", "daemon off;"] # 以 exec 格式启动 Nginx
    CMD python3 app.py # 以 shell 格式启动 Python 应用
    最佳实践:推荐使用 exec 格式,因为它更清晰,且避免了 shell 的额外开销。

8. ENTRYPOINT

  • 作用:提供容器启动时要执行的固定命令或脚本。它不会被 docker run 命令中的参数覆盖,而是将 docker run 命令的参数作为其自身的参数。一个 Dockerfile 中只能有一条 ENTRYPOINT 指令。
  • 语法ENTRYPOINT ["executable", "param1", "param2"] (exec 格式,推荐)
  • 示例
    1
    2
    ENTRYPOINT ["/usr/bin/supervisord"] # 总是以 supervisord 启动
    CMD ["-c", "/etc/supervisor/conf.d/supervisord.conf"] # CMD 作为 ENTRYPOINT 的默认参数
    在这种组合下,docker run my_image 会执行 /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf。如果 docker run my_image ls -l,则会执行 /usr/bin/supervisord ls -l

CMDENTRYPOINT 的区别总结:

特性CMDENTRYPOINT
覆盖可被 docker run 后面的命令覆盖不会被 docker run 后面的命令覆盖,而是将其作为参数
用途提供默认命令或 ENTRYPOINT 的默认参数定义容器的主入口点(可执行文件/脚本)
数量一个 Dockerfile 中只能有一条一个 Dockerfile 中只能有一条

9. ENV

  • 作用:设置环境变量。这些变量在镜像构建时和容器运行时都可用。
  • 语法ENV <key>=<value> ...
  • 示例
    1
    2
    ENV APP_PORT=5000
    ENV DATABASE_URL=mongodb://localhost:27017/myapp

10. ARG

  • 作用:定义构建时变量。这些变量可以在 docker build 命令中通过 --build-arg 参数传递。它们只在构建过程中有效,容器运行时不会保留。
  • 语法ARG <name>[=<default value>]
  • 示例
    1
    2
    ARG NODE_VERSION=16
    FROM node:${NODE_VERSION}-alpine

11. VOLUME

  • 作用:声明一个挂载点,用于将数据从容器文件系统分离,实现数据持久化或共享。
  • 语法VOLUME ["/data"]VOLUME /data
  • 示例
    1
    VOLUME /var/lib/mysql # 声明 /var/lib/mysql 目录为数据卷
    这仅是声明一个意图,实际的卷挂载需要在 docker run -vdocker-compose.yml 中指定。

12. USER

  • 作用:设置执行后续指令或容器启动时使用的用户或用户组。
  • 语法USER <user>[:<group>]
  • 示例
    1
    USER appuser # 后续操作将以 appuser 身份运行
    最佳实践:为了安全,尽量避免使用 root 用户运行容器进程。

构建一个简单的 Flask 应用 Dockerfile 示例

现在,我们来构建一个包含 Python Flask 应用的 Docker 镜像。

首先,准备以下文件:

app.py (Flask 应用代码):

1
2
3
4
5
6
7
8
9
10
from flask import Flask

app = Flask(__name__)

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

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

requirements.txt (Python 依赖):

Flask==2.0.3

Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 指定基础镜像:我们选择一个轻量级的 Python 镜像
FROM python:3.9-slim-buster

# 2. 设置工作目录:后续的 COPY 和 CMD 指令都将在这个目录下执行
WORKDIR /app

# 3. 拷贝依赖文件到镜像中,以便安装
# 这里的顺序很重要:先拷贝依赖文件,再安装。这样如果依赖文件不变,可以利用 Docker 的缓存。
COPY requirements.txt .

# 4. 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt

# 5. 拷贝应用程序代码到镜像中
COPY . .

# 6. 声明容器将监听 5000 端口
EXPOSE 5000

# 7. 定义容器启动时执行的默认命令
# 这里使用 exec 格式,推荐
CMD ["python", "app.py"]

构建镜像:

在包含 app.py, requirements.txtDockerfile 的目录下,打开终端并执行:

1
docker build -t my-flask-app:1.0 .

-t my-flask-app:1.0 为你的镜像指定了名称和标签,. 表示 Dockerfile 位于当前目录。

运行容器:

1
docker run -d -p 5000:5000 --name flask-web-app my-flask-app:1.0

运行后,您可以在浏览器中访问 http://localhost:5000,看到 Hello, Docker World!

Dockerfile 构建缓存机制

Docker 在构建镜像时会逐行执行 Dockerfile 中的指令。每条指令的执行结果都会被缓存为一个新的镜像层。如果某条指令及其上下文没有发生变化,Docker 会直接使用缓存层,从而大大加快构建速度。

利用缓存的策略:

  • 将不经常变化的指令放在 Dockerfile 的前面:例如 FROMCOPY requirements.txtRUN pip install
  • 将经常变化的指令放在 Dockerfile 的后面:例如 COPY . . (应用程序代码)。
  • 避免在 RUN 指令中进行不必要的变更:例如,将 apt-get updateapt-get install 写在一起,避免 apt-get update 单独一层导致后续缓存失效。

结语

本篇文章深度解析了 Dockerfile 的常用指令,并通过一个实际的 Flask 应用构建示例,让您了解了如何编写 Dockerfile 并构建自己的镜像。掌握 Dockerfile 是将应用程序容器化的核心技能。

在下一篇文章中,我们将学习如何管理容器的数据。虽然容器本身是临时的,但应用程序的数据往往需要持久化存储。我们将深入探讨 Docker Volume (数据卷) 的概念和用法。