Docker 容器技术入门与实践 (二):Dockerfile文件

Dockerfile 文件详解

摘要: Dockerfile 是构建 Docker 镜像的核心蓝图文件。本文深入解析 Dockerfile 的每一个常用指令及其作用,并结合 OpenEuler 操作系统,通过一个部署 Discuz 论坛的综合实例,详细阐述 Dockerfile 的编写要点、最佳实践以及镜像构建过程。内容涵盖理论讲解、命令详解、实例代码分析,旨在帮助读者掌握利用 Dockerfile 定制化构建容器镜像的核心技能。

关键词: Docker, Dockerfile, OpenEuler, 容器化, 镜像构建, Discuz


第一部分:Dockerfile 概述与核心作用

在 Docker 生态中,镜像 (Image) 是容器运行的基础模板,而 Dockerfile 则是一个纯文本文件,其中包含了一系列用于构建 Docker 镜像的指令和说明。它是定义镜像构建过程的自动化脚本。

Dockerfile 的核心作用:

  1. 自动化构建: 取代了手动执行一系列命令来创建镜像的繁琐过程。只需编写一个 Dockerfile,运行 docker build 命令,Docker 引擎便会按照文件中的指令顺序自动执行构建。
  2. 可重复性与一致性: 确保了无论在哪个环境(开发、测试、生产),只要基于同一个 Dockerfile 构建镜像,得到的结果都是完全一致的。这消除了"在我机器上能运行"的环境差异问题。
  3. 版本控制与审计: Dockerfile 是纯文本文件,可以像管理应用程序源代码一样,将其纳入版本控制系统(如 Git)。这使得镜像构建过程的变更历史清晰可追溯,便于协作和审计。
  4. 分层构建与高效利用: Docker 镜像采用分层存储结构。Dockerfile 中的每条指令都会创建一个新的镜像层。如果 Dockerfile 或构建上下文未改变,后续构建可以重用这些缓存层,极大提高构建速度。层共享也节省了存储空间。
  5. 定义运行环境: Dockerfile 精确地定义了容器运行所需的基础操作系统、软件包、依赖库、环境变量、配置文件、启动命令等,为应用程序提供了一个隔离、可预测的运行沙箱。
  6. 最佳实践固化: 可以在 Dockerfile 中固化安全配置(如使用非 root 用户)、优化措施(如清理缓存)等最佳实践。

Dockerfile 与 OpenEuler: OpenEuler 是一个开源的企业级 Linux 发行版。使用 Dockerfile 在 OpenEuler 基础镜像上构建自定义镜像,能够快速、标准化地部署基于 OpenEuler 的应用程序环境,充分发挥容器化带来的优势。


第二部分:Dockerfile 常用指令详解

下面详细介绍 Dockerfile 中最常用且关键的指令,理解它们的作用和用法是编写有效 Dockerfile 的基础。

  1. FROM

    • 作用: 指定构建新镜像所基于的基础镜像 。这必须是 Dockerfile 文件的第一条有效指令 (注释和 ARG 指令除外)。后续的构建步骤将在该基础镜像提供的环境中进行。

    • 格式:

      dockerfile 复制代码
      FROM <image>[:<tag>] [AS <name>]
    • 说明:

      • <image>:基础镜像的名称,可以是官方镜像(如 openeuler/openeuler)、仓库镜像(如 registry.example.com/myapp)或本地镜像。
      • <tag>:指定镜像的版本标签(如 :22.03)。强烈建议始终指定明确的标签(而非默认的 latest)以保证构建的可重复性。
      • [AS <name>]:可选,为构建阶段设置一个名称,用于多阶段构建中后续 FROMCOPY --from=<name> 指令引用此阶段。
    • OpenEuler 示例:

      dockerfile 复制代码
      # 使用官方 OpenEuler 22.03 LTS 基础镜像
      FROM openeuler/openeuler:22.03
  2. LABEL

    • 作用: 为镜像添加元数据信息。这些信息是键值对形式,用于描述镜像的作者、版本、描述、许可证等信息。有助于镜像的管理和识别。

    • 格式:

      dockerfile 复制代码
      LABEL <key>=<value> [<key>=<value> ...]
    • 说明: 可以在一行添加多个标签,或使用多条 LABEL 指令。

    • 示例:

      dockerfile 复制代码
      LABEL maintainer="your.email@example.com"
      LABEL version="1.0"
      LABEL description="Customized OpenEuler image with LAMP stack"
      # 或者合并
      LABEL maintainer="your.email@example.com" \
            version="1.0" \
            description="Customized OpenEuler image with LAMP stack"
  3. RUN

    • 作用: 在构建过程中,在当前镜像层 执行指定的 Shell 命令或可执行程序。通常用于安装软件包、编译代码、创建目录、修改文件等操作。

    • 格式:

      dockerfile 复制代码
      # Shell 格式 (默认在 /bin/sh -c 下执行)
      RUN <command>
      # Exec 格式 (直接执行,不通过 shell)
      RUN ["executable", "param1", "param2"]
    • 说明:

      • Shell 格式: 更常见,可以使用环境变量和命令链(如 &&||;)。
      • Exec 格式: 避免了 shell 字符串处理,参数是数组形式,在某些情况下更安全。
      • OpenEuler 注意事项: OpenEuler 使用 dnf 包管理器(兼容 yum)。安装软件包时,通常使用 RUN dnf install -y <package>-y 参数自动确认安装。
      • 优化: 将多个 RUN 指令合并为一个(使用 &&\ 换行),可以减少镜像层数,提高构建效率和最终镜像的紧凑性。清理缓存 (dnf clean all) 也是常见做法。
    • 示例:

      dockerfile 复制代码
      RUN dnf update -y && \
          dnf install -y php php-fpm php-mysqlnd php-gd php-mbstring nginx mariadb-server && \
          dnf clean all
  4. COPY

    • 作用:构建上下文 中的文件或目录复制到镜像内的指定路径。

    • 格式:

      dockerfile 复制代码
      COPY [--chown=<user>:<group>] <src>... <dest>
      # 或者
      COPY [--chown=<user>:<group>] ["<src>", ... "<dest>"] (对于包含空格的路径)
    • 说明:

      • <src>:源文件或目录,路径是相对于构建上下文 的路径。不能使用绝对路径(如 /home/user/file),只能使用相对路径(如 ./configapp)。可以使用通配符(如 *.txt)。
      • <dest>:目标路径,是镜像内的绝对路径或相对于 WORKDIR 的路径(如果已设置)。
      • --chown=<user>:<group>:可选,设置复制到镜像内文件的所有权和权限。例如 --chown=nginx:nginx
      • ADD 的区别: COPY 是更纯粹的文件复制指令。ADD 功能更多(如自动解压 tar 包、从 URL 下载),但行为有时不够清晰。推荐优先使用 COPY 进行文件复制,除非需要 ADD 的特定功能。
    • 示例:

      dockerfile 复制代码
      # 复制本地的 Nginx 配置文件到镜像的 /etc/nginx/conf.d/ 目录
      COPY nginx.conf /etc/nginx/conf.d/default.conf
      # 复制整个项目目录到镜像的 /var/www/html/
      COPY ./discuz /var/www/html/discuz
      # 复制并改变所有权
      COPY --chown=nginx:nginx ./upload /var/www/html/discuz/upload
  5. ADD

    • 作用: 功能比 COPY 更丰富,除了复制本地文件,还支持:

      • 自动解压:如果 <src> 是本地压缩文件(如 .tar, .gz, .bz2, .xz),ADD 会自动将其解压到 <dest>
      • 从 URL 下载:如果 <src> 是一个 URL,ADD 会从该 URL 下载文件并复制到 <dest>(注意:下载的文件不会自动解压)。
    • 格式:COPY

    • 说明:

      • 谨慎使用: 自动解压功能在某些场景下方便,但也可能带来意外行为(如解压不想解压的文件)。从 URL 下载不如在 RUN 指令中使用 wgetcurl 明确可控。最佳实践是仅在需要自动解压时才使用 ADD,否则优先使用 COPY
    • 示例:

      dockerfile 复制代码
      # 下载并复制一个 tar 包(不会自动解压)
      ADD https://example.com/source.tar.gz /tmp/
      # 复制并自动解压本地 tar 包
      ADD source.tar.gz /usr/local/src/
  6. CMD

    • 作用: 为容器指定默认的启动命令及其参数 。一个 Dockerfile 中只能有一条 CMD 指令。如果定义了多条,只有最后一条生效。CMD 的主要目的是在启动容器时提供一个默认的执行命令。

    • 格式:

      dockerfile 复制代码
      # Shell 格式 (在 /bin/sh -c 下执行)
      CMD <command>
      # Exec 格式 (推荐)
      CMD ["executable", "param1", "param2"]
      # 作为 ENTRYPOINT 的参数
      CMD ["param1", "param2"] (需配合 ENTRYPOINT 的 Exec 格式使用)
    • 说明:

      • docker run 覆盖: 如果在运行容器时使用 docker run <image> <command> 指定了命令,则会覆盖 Dockerfile 中的 CMD 指令。
      • ENTRYPOINT 关系:ENTRYPOINT 使用 Exec 格式时,CMD 的内容会作为参数传递给 ENTRYPOINT 指定的命令。
      • 推荐 Exec 格式: 避免 shell 处理,信号处理更直接。
    • 示例:

      dockerfile 复制代码
      # 启动 Nginx
      CMD ["nginx", "-g", "daemon off;"]
      # 启动 PHP-FPM
      CMD ["php-fpm", "-F"]
  7. ENTRYPOINT

    • 作用: 指定容器启动时运行的主命令 (可执行文件)。它类似于 CMD,但设计目的是让容器像一个可执行程序一样运行。

    • 格式:CMD (Shell 或 Exec 格式)。

    • 说明:

      • docker run 覆盖: 使用 docker run --entrypoint=<command> 可以覆盖 ENTRYPOINT
      • CMD 交互:
        • 如果 ENTRYPOINT 是 Exec 格式,CMD 提供参数。docker run <image> <arg> 会覆盖 CMD 的参数。
        • 如果 ENTRYPOINT 是 Shell 格式,CMD 会被忽略。docker run <image> <command> 会覆盖整个命令(包括 ENTRYPOINT 和可能的 CMD)。
      • 推荐 Exec 格式: 原因同 CMD
      • 用途: 当你想让容器运行一个固定的命令(如 mysql, redis-server),但允许用户传递参数时非常有用。
    • 示例:

      dockerfile 复制代码
      # 设置 MariaDB 为入口点,CMD 提供默认参数
      ENTRYPOINT ["mysqld"]
      CMD ["--user=mysql"]
      # 运行容器时 `docker run mydb --datadir=/custom/data` 会覆盖 CMD 参数
  8. WORKDIR

    • 作用: 设置后续 RUN, CMD, ENTRYPOINT, COPY, ADD 等指令在容器内的工作目录。如果目录不存在,会自动创建。

    • 格式:

      dockerfile 复制代码
      WORKDIR /path/to/workdir
    • 说明: 可以多次使用 WORKDIR,后续路径如果是相对路径,将基于前一个 WORKDIR 路径。使用绝对路径更清晰。

    • 示例:

      dockerfile 复制代码
      WORKDIR /var/www/html
      RUN touch index.php # 在 /var/www/html 下创建 index.php
      WORKDIR /app
      COPY . . # 将构建上下文复制到 /app
  9. ENV

    • 作用: 设置环境变量。这些变量在构建阶段和容器运行阶段都可用。后续的指令和容器内运行的进程都可以访问这些变量。

    • 格式:

      dockerfile 复制代码
      ENV <key>=<value> ...
    • 说明: 可以在一行设置多个环境变量,或使用多条 ENV 指令。设置的环境变量会持久化到镜像中。

    • 示例:

      dockerfile 复制代码
      ENV MYSQL_ROOT_PASSWORD=mysecretpassword
      ENV APP_ENV=production \
          TZ=Asia/Shanghai
  10. ARG

    • 作用: 定义一个在构建阶段 使用的变量。其值可以通过 docker build 命令的 --build-arg <varname>=<value> 选项传入。ARG 指令定义的变量在容器运行阶段不可用

    • 格式:

      dockerfile 复制代码
      ARG <name>[=<default_value>]
    • 说明:

      • 可以有默认值。如果没有默认值,且在构建时未提供值,则构建会出错。
      • 作用范围:ARG 指令在定义之后生效。可以使用多个 ARG
      • 使用:可以在 RUN 指令中通过 $varname${varname} 使用。
      • 预定义 ARG: Docker 提供了一些预定义的 ARG 变量,如 HTTP_PROXY, HTTPS_PROXY, NO_PROXY, http_proxy 等,可以在构建时传递这些值来影响网络行为。
    • 示例:

      dockerfile 复制代码
      ARG USER_NAME=defaultuser
      ARG USER_UID=1000
      RUN useradd -u $USER_UID $USER_NAME
      # 构建时指定: docker build --build-arg USER_NAME=myuser --build-arg USER_UID=1001 -t myimage .
  11. EXPOSE

    • 作用: 声明 容器在运行时监听网络端口 。这只是一个元数据声明,并不会自动在主机上打开端口。主要作用是告知使用者(开发或运维人员)该容器设计上需要暴露哪些端口。

    • 格式:

      dockerfile 复制代码
      EXPOSE <port> [<port>/<protocol>...]
    • 说明:

      • 可以指定端口号(如 80)或端口号加协议(如 80/tcp)。默认协议是 TCP。
      • 实际端口映射需要在运行容器时通过 docker run -p <host_port>:<container_port> 或 Docker Compose 的 ports 配置来完成。
    • 示例:

      dockerfile 复制代码
      EXPOSE 80/tcp  # 声明监听 80 端口 (TCP)
      EXPOSE 3306    # 声明监听 3306 端口 (默认 TCP)
  12. VOLUME

    • 作用: 在镜像中创建一个挂载点 ,用于将外部存储(宿主机目录、其他容器的卷、命名卷)持久化地挂载到容器中。它定义了容器内哪些路径的数据应该被持久化,避免写入容器可写层(提高性能、数据安全)。

    • 格式:

      dockerfile 复制代码
      VOLUME ["/path/to/volume"]
      # 或者
      VOLUME /path/to/volume /another/path
    • 说明:

      • 这只是一个声明,告诉 Docker 这个目录需要挂载外部存储。
      • 实际挂载操作是在运行容器时通过 docker run -v 或 Docker Compose 的 volumes 配置完成的。
      • 如果运行时未指定挂载源,Docker 会自动创建一个匿名的命名卷挂载到该路径。
    • 示例:

      dockerfile 复制代码
      VOLUME /var/lib/mysql # MariaDB/MySQL 数据目录
      VOLUME /var/www/html/discuz/upload # Discuz 上传目录
      VOLUME /var/log/nginx /var/log/php-fpm # 日志目录
  13. USER

    • 作用: 设置后续 RUN, CMD, ENTRYPOINT 等指令执行时的用户身份 (以及可选的用户组)。这对于安全非常重要,避免容器内进程以 root 权限运行。

    • 格式:

      dockerfile 复制代码
      USER <user>[:<group>]
      USER <UID>[:<GID>]
    • 说明:

      • 可以是用户名(或用户ID)和组名(或组ID)。
      • 需要确保该用户/组在镜像中已存在(通常通过 RUN useradd 创建)。
      • 设置后,后续指令将以该用户权限执行。容器启动时的主进程也以该用户运行。
    • 示例:

      dockerfile 复制代码
      RUN groupadd -r nginx && useradd -r -g nginx nginx
      USER nginx
      CMD ["nginx", "-g", "daemon off;"]
  14. ONBUILD

    • 作用: 向镜像添加一个触发器 指令。这个指令不会在当前构建中执行,而是会在另一个镜像以当前镜像为基础镜像进行构建时触发执行。

    • 格式:

      dockerfile 复制代码
      ONBUILD <INSTRUCTION>
    • 说明: 常用于创建基础镜像(如 FROM openjdk:11),在该基础镜像的 Dockerfile 中使用 ONBUILD 来定义一些子镜像构建时需要的通用操作(如复制源码、运行构建命令)。子镜像构建时会执行这些 ONBUILD 指令。

    • 示例:

      dockerfile 复制代码
      # 在一个基础应用镜像中
      ONBUILD COPY . /app
      ONBUILD RUN make /app
  15. STOPSIGNAL

    • 作用: 设置容器停止时发送给主进程的系统信号 。默认是 SIGTERM

    • 格式:

      dockerfile 复制代码
      STOPSIGNAL signal
    • 说明: 例如,STOPSIGNAL SIGINTSTOPSIGNAL 9(对应 SIGKILL)。一般使用默认即可。

  16. HEALTHCHECK

    • 作用: 定义如何检查容器是否健康运行 。Docker 可以根据此指令的状态决定容器的健康状态(healthy, unhealthy, starting)。

    • 格式:

      dockerfile 复制代码
      HEALTHCHECK [OPTIONS] CMD <command> (在容器内执行命令检查健康)
      HEALTHCHECK NONE (禁用任何基础镜像继承的健康检查)
    • 选项:

      • --interval=DURATION (默认: 30s):检查间隔。
      • --timeout=DURATION (默认: 30s):检查超时时间。
      • --start-period=DURATION (默认: 0s):容器启动后等待多长时间开始检查。用于避免启动过程中的临时失败。
      • --retries=N (默认: 3):连续失败 N 次后标记为 unhealthy。
    • 命令: 命令的退出状态决定健康状态:

      • 0: 成功 - 容器健康。
      • 1: 失败 - 容器不健康。
      • 2: 保留 - 不使用。
    • 示例:

      dockerfile 复制代码
      HEALTHCHECK --interval=1m --timeout=10s --retries=3 \
        CMD curl -f http://localhost/ || exit 1
  17. SHELL

    • 作用: 覆盖 Dockerfile 中 RUN, CMD, ENTRYPOINT 指令使用的默认 shell 。默认是 ["/bin/sh", "-c"]

    • 格式:

      dockerfile 复制代码
      SHELL ["executable", "parameters"]
    • 示例: 在 Windows 容器中切换到 PowerShell:

      dockerfile 复制代码
      SHELL ["powershell", "-command"]

第三部分:Dockerfile 编写要点与最佳实践

在编写 Dockerfile 时,遵循以下要点和最佳实践可以构建出更高效、安全、可维护的镜像:

  1. 选择合适的基础镜像:

    • 优先选择官方维护 的基础镜像(如 openeuler/openeuler, nginx, mysql)。
    • 尽量选择体积小安全稳定 的镜像版本(如 Alpine Linux 变体,或特定版本的 OpenEuler)。避免使用 latest 标签。
    • 对于 OpenEuler,使用 openeuler/openeuler:<tag>
  2. 最小化镜像层数:

    • 将相关的 RUN 指令合并,使用 && 连接命令,并用 \ 换行提高可读性。
    • 减少不必要的 COPYADD 指令。
    • RUN 指令中安装软件后,记得清理缓存 (如 dnf clean all, rm -rf /var/cache/yum)以减小镜像大小。
  3. 利用构建缓存:

    • Docker 在构建时会重用之前构建的缓存层。将最不易变化的指令 (如安装基础包)放在前面,将易变化的指令(如复制应用程序代码)放在后面。这样当代码改变时,只需重建后面的层。
  4. .dockerignore 文件:

    • 创建一个 .dockerignore 文件(类似于 .gitignore),列出不需要复制到构建上下文(从而不会进入镜像)的文件和目录(如 .git, node_modules, *.log, *.tmp)。这能减少构建上下文大小,加速构建过程。
  5. 安全性:

    • 避免以 root 用户运行: 使用 USER 指令切换到非特权用户。在 RUN 指令中创建必要的用户和组。
    • 谨慎处理敏感数据: 不要在 Dockerfile 中硬编码密码、密钥。使用 ARG 在构建时传入(注意构建历史可能暴露),或者更安全地,在运行容器时通过环境变量 (docker run -e) 或 Docker Secrets 传入。
    • 及时更新软件: 在基础镜像和 RUN dnf update -y 中确保使用最新的安全补丁。
  6. 清晰性与可维护性:

    • 使用注释解释指令的目的和复杂步骤。
    • 遵循一致的格式化命名约定。
    • ENV 用于常用的路径或配置,提高可读性。
    • 使用多阶段构建(Multi-stage builds)分离构建环境和运行时环境,使最终镜像更小。例如,在一个阶段编译代码,在另一个阶段只复制编译好的二进制文件。
  7. COPY 优于 ADD 除非需要 ADD 的特定功能(自动解压或 URL 下载),否则优先使用语义更清晰的 COPY

  8. CMDENTRYPOINT 的使用:

    • 理解两者的区别和交互方式。
    • 优先使用 Exec 格式
    • 如果容器运行的是一个单一的可执行程序,使用 ENTRYPOINT 定义它,用 CMD 提供默认参数。
    • 如果需要灵活性让用户传递不同命令,使用 CMD
  9. 持久化数据: 使用 VOLUME 声明需要持久化数据的目录(如数据库文件、日志、用户上传内容)。

  10. 健康检查: 为需要长时间运行的服务(如 Web 服务器、数据库)配置 HEALTHCHECK,便于 Docker 监控容器状态。

  11. 测试: 构建镜像后,运行容器进行测试,验证应用程序功能、配置和启动命令是否正常工作。


第四部分:综合实例 - 在 OpenEuler 上使用 Dockerfile 部署 Discuz 论坛

场景描述: 我们将创建一个 Dockerfile,基于 OpenEuler 22.03 LTS 基础镜像,构建一个包含 Nginx、PHP-FPM、MariaDB 的 LAMP 环境,并部署 Discuz X 论坛系统。镜像将配置好数据库初始化、Discuz 安装文件、Nginx 虚拟主机和必要的权限。

步骤分解:

  1. 项目结构准备:

    复制代码
    discuz-docker/
    ├── Dockerfile
    ├── docker-compose.yml (可选,用于简化运行)
    ├── config/
    │   ├── nginx/
    │   │   └── discuz.conf (Nginx 配置文件)
    │   ├── php/
    │   │   └── php.ini (PHP 配置文件覆盖)
    │   └── mariadb/
    │       └── initdb.sql (数据库初始化脚本)
    ├── scripts/
    │   └── init.sh (容器启动初始化脚本)
    └── src/
        └── discuz/ (Discuz 程序文件,从官网下载解压)
    • src/discuz/:从 Discuz 官网 (https://www.discuz.net) 下载最新的 Discuz X 版本(如 Discuz_X3.5_SC_UTF8.zip),解压后放置于此目录。确保目录结构正确。
    • config/nginx/discuz.conf:Nginx 的虚拟主机配置。
    • config/php/php.ini:可能需要覆盖的 PHP 配置(如 upload_max_filesize, post_max_size)。
    • config/mariadb/initdb.sql:创建 Discuz 数据库、用户和基本权限的 SQL 脚本。
    • scripts/init.sh:一个 shell 脚本,在容器首次运行时执行数据库初始化、文件权限设置等操作。
  2. 编写 Dockerfile

dockerfile 复制代码
# 阶段一:构建基础 LAMP 环境
FROM openeuler/openeuler:22.03 AS base

# 设置元数据标签
LABEL maintainer="your.email@example.com"
LABEL version="1.0"
LABEL description="Discuz X Forum on OpenEuler with Docker"

# 设置时区 (可选)
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# 更新系统并安装核心软件 (Nginx, PHP-FPM, MariaDB, 工具)
RUN dnf update -y && \
    dnf install -y nginx \
                   php-fpm php-mysqlnd php-gd php-mbstring php-curl php-xml php-zip \
                   mariadb-server \
                   tar gzip unzip curl && \
    dnf clean all

# 配置 PHP-FPM (允许环境变量)
RUN sed -i 's/;clear_env = no/clear_env = no/' /etc/php-fpm.d/www.conf && \
    sed -i 's/user = apache/user = nginx/' /etc/php-fpm.d/www.conf && \
    sed -i 's/group = apache/group = nginx/' /etc/php-fpm.d/www.conf

# 配置 Nginx 运行用户
RUN sed -i 's/user nginx;/#user nginx;/' /etc/nginx/nginx.conf # 或者确保用户存在且权限正确

# 创建 Nginx 用户和组 (确保存在)
RUN groupadd -r nginx && useradd -r -g nginx nginx

# 创建必要的目录
RUN mkdir -p /var/www/html && \
    mkdir -p /var/log/nginx && \
    mkdir -p /var/log/php-fpm && \
    mkdir -p /run/php-fpm

# 设置工作目录
WORKDIR /var/www/html

# 复制配置文件
COPY config/nginx/discuz.conf /etc/nginx/conf.d/default.conf
COPY config/php/php.ini /etc/php.d/zz-overrides.ini

# 复制 Discuz 源代码
COPY src/discuz /var/www/html/discuz

# 复制数据库初始化脚本
COPY config/mariadb/initdb.sql /docker-entrypoint-initdb.d/

# 复制初始化脚本并赋予执行权限
COPY scripts/init.sh /usr/local/bin/init-discuz
RUN chmod +x /usr/local/bin/init-discuz

# 设置持久化卷 (数据、上传、日志)
VOLUME /var/lib/mysql
VOLUME /var/www/html/discuz/upload
VOLUME /var/www/html/discuz/data
VOLUME /var/log/nginx
VOLUME /var/log/php-fpm

# 暴露端口 (HTTP)
EXPOSE 80

# 设置健康检查 (检查 Nginx)
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD curl -f http://localhost/ || exit 1

# 阶段二:最终运行阶段 (基于 base 阶段)
FROM base

# 设置容器启动命令 (一个启动脚本会更好,这里简化)
# 注意:实际生产环境可能需要一个更复杂的 entrypoint 脚本来协调多个服务启动
# 这里使用一个简单的 init 脚本作为入口点
ENTRYPOINT ["/usr/local/bin/init-discuz"]

关键指令详解 (针对 Discuz 实例):

  • FROM openeuler/openeuler:22.03 AS base: 明确使用 OpenEuler 22.03 作为基础镜像,并命名此阶段为 base

  • RUN dnf install ...: 一次性安装所有必需的软件包(Nginx, PHP-FPM + 扩展, MariaDB, 工具),并清理缓存。

  • RUN sed ...: 修改 PHP-FPM 配置文件 (www.conf):

    • clear_env = no: 允许 PHP 进程继承环境变量(对容器化有用)。
    • 将运行用户和组从 apache 改为 nginx(与我们将使用的 Nginx 用户匹配)。
  • RUN groupadd ... useradd ...: 创建 nginx 用户和组。虽然基础镜像可能已有,但明确创建更可靠。

  • WORKDIR /var/www/html: 设置 Web 根目录为工作目录。

  • COPY config/nginx/discuz.conf ...: 将定制的 Nginx 虚拟主机配置复制到容器中,覆盖默认配置。discuz.conf 应包含指向 /var/www/html/discuzroot 指令和 PHP-FPM 的 fastcgi_pass 配置。

  • COPY config/php/php.ini ...: 覆盖 PHP 配置,例如增大上传文件限制:

    ini 复制代码
    upload_max_filesize = 20M
    post_max_size = 20M
  • COPY src/discuz ...: 将下载解压好的 Discuz 程序文件复制到 Web 根目录下的 discuz 子目录。

  • COPY config/mariadb/initdb.sql /docker-entrypoint-initdb.d/: MariaDB 容器有一个特性:如果 /docker-entrypoint-initdb.d/ 目录存在,在数据库首次初始化时会按字母顺序执行该目录下的 .sh, .sql, .sql.gz 文件。我们将数据库初始化脚本放在这里。initdb.sql 内容示例:

    sql 复制代码
    CREATE DATABASE IF NOT EXISTS discuz DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    CREATE USER 'discuz'@'%' IDENTIFIED BY 'your_strong_password'; -- 生产环境应用 ARG 或运行时传入
    GRANT ALL PRIVILEGES ON discuz.* TO 'discuz'@'%';
    FLUSH PRIVILEGES;

    注意: 示例中硬编码了密码。实际生产中,应使用 ARG 或运行时环境变量传入密码!

  • COPY scripts/init.sh ...: 复制一个初始化脚本并赋予执行权限。这个脚本将是容器的入口点 (ENTRYPOINT)。init.sh 示例内容:

    bash 复制代码
    #!/bin/sh
    set -e # 遇到错误退出
    
    # 1. 处理文件权限 (确保 Nginx/PHP 能读写所需目录)
    chown -R nginx:nginx /var/www/html/discuz
    find /var/www/html/discuz -type d -exec chmod 755 {} \;
    find /var/www/html/discuz -type f -exec chmod 644 {} \;
    chown -R nginx:nginx /var/www/html/discuz/upload
    chown -R nginx:nginx /var/www/html/discuz/data
    chown -R nginx:nginx /var/log/nginx
    chown -R nginx:nginx /var/log/php-fpm
    
    # 2. 启动 MariaDB (后台运行)
    if [ ! -d "/var/lib/mysql/mysql" ]; then
        echo "Initializing MariaDB database..."
        mysql_install_db --user=mysql --datadir=/var/lib/mysql > /dev/null
    fi
    mysqld_safe --user=mysql --datadir=/var/lib/mysql &
    
    # 等待 MariaDB 启动 (简单方法)
    sleep 10
    
    # 3. 启动 PHP-FPM (后台运行)
    php-fpm -F &
    
    # 4. 启动 Nginx (前台运行,保持容器不退出)
    echo "Starting Nginx..."
    exec nginx -g 'daemon off;'
    • 这个脚本执行了关键的初始化步骤:
      • 权限设置: 确保 Discuz 目录、上传目录、数据目录、日志目录的所有权和权限正确。
      • 启动 MariaDB: 检查数据库是否已初始化,如果没有则初始化 (mysql_install_db),然后启动 MariaDB 服务 (mysqld_safe) 在后台运行。
      • 启动 PHP-FPM: 在后台运行 PHP-FPM。
      • 启动 Nginx: 使用 exec 在前台启动 Nginx (nginx -g 'daemon off;')。exec 会替换当前 shell 进程,使得 Nginx 成为容器的主进程 (PID 1)。这样 Docker 可以正确管理 Nginx 的信号。Nginx 保持前台运行也使得容器不会立即退出。
    • 注意: 这是一个简化示例。生产环境可能需要更健壮的脚本(如使用 wait-for 脚本等待数据库真正就绪)。
  • VOLUME ...: 声明了多个持久化卷挂载点:MariaDB 数据目录、Discuz 上传目录、Discuz 数据目录、Nginx 和 PHP-FPM 日志目录。确保用户数据、上传文件和日志在容器重启或重建后不会丢失。

  • EXPOSE 80: 声明容器监听 80 端口。

  • HEALTHCHECK ...: 配置健康检查,定期使用 curl 检查本地 Nginx 是否响应,失败 3 次则标记为 unhealthy。

  • FROM base: 最终镜像基于之前构建的 base 阶段。这里没有新的指令,主要是为了清晰结构。如果使用多阶段构建优化(如编译某些扩展),这里会更明显。

  • ENTRYPOINT ["/usr/local/bin/init-discuz"]: 设置初始化脚本 /usr/local/bin/init-discuz(即我们之前复制的 init.sh)作为容器的主入口点。当容器启动时,将执行此脚本。

  1. 构建镜像:

    • 在包含 Dockerfilediscuz-docker 目录下运行:

      bash 复制代码
      docker build -t my-discuz:1.0 .
    • Docker 引擎将根据 Dockerfile 逐步执行指令,构建名为 my-discuz,标签为 1.0 的镜像。

  2. 运行容器:

    • 使用 docker run 命令启动容器,映射端口并挂载卷:

      bash 复制代码
      docker run -d --name discuz-container \
        -p 8080:80 \ # 将容器 80 端口映射到宿主机 8080
        -v discuz-mysql-data:/var/lib/mysql \ # 命名卷存储数据库
        -v discuz-uploads:/var/www/html/discuz/upload \ # 命名卷存储上传
        -v discuz-data:/var/www/html/discuz/data \ # 命名卷存储 Discuz 数据
        -v ./logs/nginx:/var/log/nginx \ # 绑定宿主机目录存储 Nginx 日志
        -v ./logs/php-fpm:/var/log/php-fpm \ # 绑定宿主机目录存储 PHP-FPM 日志
        my-discuz:1.0
    • 解释:

      • -d: 后台运行。
      • --name: 指定容器名称。
      • -p 8080:80: 将容器内部的 80 端口映射到宿主机的 8080 端口。访问 http://localhost:8080/ 即可访问 Discuz。
      • -v ...: 挂载卷。这里使用了命名卷 (discuz-mysql-data 等) 用于持久化数据库、上传和 Discuz 应用数据。日志则直接绑定挂载到宿主机的 ./logs 目录下方便查看。
      • 数据库密码: 示例 initdb.sql 中硬编码了密码。更安全的方式是:
        • initdb.sql 中使用占位符 (如 CREATE USER 'discuz'@'%' IDENTIFIED BY '$MYSQL_PASSWORD';)。
        • init.sh 脚本中,使用 sed 或环境变量替换占位符(脚本需要处理替换)。
        • 在运行容器时通过 -e MYSQL_PASSWORD=your_strong_password 设置环境变量。
  3. 访问 Discuz 进行安装:

    • 打开浏览器访问 http://localhost:8080/discuz/install/ (根据实际映射端口调整)。
    • 按照 Discuz 的网页安装向导进行操作。
    • 数据库配置步骤: 在安装向导的数据库设置页面:
      • 数据库服务器: localhost (因为 MySQL 和 Discuz 在同一容器内)
      • 数据库名: discuz (在 initdb.sql 中创建)
      • 用户名: discuz (在 initdb.sql 中创建)
      • 密码: your_strong_password (在 initdb.sql 中设置或运行时传入)
      • 表前缀: 保持默认或自定义。
    • 完成安装向导后,Discuz 论坛即可使用。
  4. (可选) 使用 Docker Compose:

    • 创建一个 docker-compose.yml 文件来简化管理和运行:

      yaml 复制代码
      version: '3.8'
      
      services:
        discuz:
          image: my-discuz:1.0 # 或者可以直接在这里 build: .
          # build: # 如果需要在 compose 中构建
          #   context: .
          #   dockerfile: Dockerfile
          container_name: discuz-app
          ports:
            - "8080:80"
          volumes:
            - discuz-mysql-data:/var/lib/mysql
            - discuz-uploads:/var/www/html/discuz/upload
            - discuz-data:/var/www/html/discuz/data
            - ./logs/nginx:/var/log/nginx
            - ./logs/php-fpm:/var/log/php-fpm
          # 环境变量示例 (需要在 init.sh 中支持)
          # environment:
          #   - MYSQL_PASSWORD=your_strong_password
          healthcheck:
            test: ["CMD", "curl", "-f", "http://localhost"]
            interval: 30s
            timeout: 10s
            retries: 3
          restart: unless-stopped
      
      volumes:
        discuz-mysql-data:
        discuz-uploads:
        discuz-data:
    • 运行:docker-compose up -d


第五部分:总结

通过本文的详细讲解和 Discuz 部署的综合实例,我们深入理解了 Dockerfile 的核心作用、每一个常用指令的用法以及编写 Dockerfile 的最佳实践。Dockerfile 是实现容器化应用构建自动化和标准化的基石。

在 OpenEuler 系统上,利用 Dockerfile 构建定制镜像,能够高效地部署如 Discuz 这样的复杂应用。实例涵盖了基础环境搭建、软件包安装、配置文件管理、持久化存储设置、服务启动协调、健康监控等关键环节,展示了 Dockerfile 在实际项目中的应用价值。

掌握 Dockerfile 的编写,使你能够灵活、可靠地构建符合需求的容器镜像,为持续集成、持续部署 (CI/CD) 和云原生应用部署奠定坚实的基础。

相关推荐
遇见火星2 小时前
linux设置开启启动服务
linux·运维·服务器·nginx
亚空间仓鼠2 小时前
Docker 容器技术入门与实践 (一):命令与镜像、容器管理
运维·docker·容器
.柒宇.2 小时前
Python 运维实战:psutil 监控系统资源 + paramiko 远程管理服务器
运维·服务器·python
王的宝库2 小时前
【K8s】集群安全机制(二):授权(Authorization)详解与实战
学习·云原生·容器·kubernetes
henry_20163 小时前
让 AI 编程助手拥有“记忆“:Mem0 OpenMemory MCP 部署到 K8s 全记录(踩坑 + 解决方案)
人工智能·ai·容器·kubernetes·kiro
东北甜妹3 小时前
Docker 多阶段构建
运维·docker·容器
Zhu7583 小时前
【软件部署】docker环境部署nagios
运维·docker·容器
IT从业者张某某3 小时前
Docker 网络
网络·docker·容器
火车叼位3 小时前
告别资源管理器卡顿:Windows 高效复制万级小文件的正确姿势
运维