搭建Hyperf本地开发环境之Docker容器开发(一)

119 阅读14分钟

写在前面

本文以部署本地开发环境,以实现快速部署为基准、方便开发为前提做说明

背景

本人在实际工作过程中,使用 hyperf 框架开发时遇到这种情况:

  • 同时开发多个项目
  • 同时使用不同的 hyperf 框架版(2.x、3.x)
  • 不同项目对接不同的数据库(Mysql、Oracle、Sqlserver)

遇到的问题有很多,比如:

  • 2.x 和 3.x 对 php 版本和 swoole 版本不一样
  • 每个 php 版本需要单独安装对应的 扩展
  • 不同的 php 版本,composer 需要重新安装
  • 项目中使用 composer 添加扩展包时,还要对应自己的项目版本使用正确的 composer
  • ... ... ...

这一套下来,耗费时间长,如果换台设备,重新部署也很麻烦;

所以,考虑到使用容器开发来解决当下的困局 ... ... ...

快速跳转

搭建Hyperf本地开发环境之Docker容器开发(二)

一、准备工作

环境准备

基于本人开发环境做说明

  • 一个 linux 环境、已安装好 docker、docker-compose
    • CentOS 版本:CentOS Linux release 7.9.2009 (Core)
    • docker 版本:Docker version 24.0.5, build ced0996
    • docker-compos 版本:Docker Compose version v2.20.2

二、开始部署(基础)

本文以 php7.4 + hyperf2.x 做开发环境的说明

准备 Dockerfile

FROM php:7.4-cli-bullseye

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

# 更新并安装基础依赖
RUN apt-get update && apt-get install -y \
    --no-install-recommends libfreetype6-dev libjpeg62-turbo-dev libpng-dev libzip-dev \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

# 安装 redis 和 swoole 扩展
RUN pecl install redis-5.3.7 swoole-4.8.13

# 移动 php.ini 配置文件
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"

# 启用扩展
RUN docker-php-ext-enable redis swoole

# 配置 GD 扩展
RUN docker-php-ext-configure gd --with-jpeg=/usr/include --with-freetype=/usr/include/freetype2/

# 安装 PHP 扩展
RUN docker-php-ext-install -j$(nproc) pcntl gd zip pdo_mysql opcache

# 添加自定义 PHP 配置
RUN echo "swoole.use_shortname='Off'" >> /usr/local/etc/php/conf.d/default.ini \
    && echo "memory_limit=1G" >> /usr/local/etc/php/conf.d/default.ini \
    && echo "opcache.enable_cli='on'" >> /usr/local/etc/php/conf.d/default.ini

# 安装 Composer
ENV COMPOSER_HOME /root/composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
ENV PATH $COMPOSER_HOME/vendor/bin:$PATH

# 指定工作目录,如果使用docker exec进入容器时,默认目录就是指定的工作目录,如/data
WORKDIR /opt/www

针对这份文档,做一些小说明:

  • 基础镜像使用 -bullseye 版本:有些基础依赖在其他版本已经不做维护了,需要自己设置对应的源地址,比较麻烦,而该版本基于最新的 debain 做兼容的
  • 该文件没有设置启动命令:是为了给多个项目自己的启动命令留空间

构建新镜像

# 执行构建
docker build -t 镜像名称:标签 .

# 检查新镜像
docker images

容器编排

docker-compose.yml

version: "3.9"
services:
        php:
                image: php-hyperf:7.4-cli-bullseye
                volumes:
                        - /etc/localtime:/etc/localtime
                        - /www:/opt/www
                restart: always
                container_name: hyperf01
#                networks:
#                        - php-net
#                ports:
#                        - 9505:9501
                network_mode: "host"
                tty: true
                stdin_open: true

#                command: ["php", "bin/hyperf.php", "start"]
#networks:
#        php-net:
#                external:
#                       name: php-net

针对这份文档,做一些小说明:

一般情况下,使用 docker-compose 编排容器是这样的:

version: "3.9"
services:
        php:
                image: php-hyperf:7.4-cli-bullseye
                volumes:
                        - /etc/localtime:/etc/localtime
                        - /www/hyperf01:/opt/www/hyperf01
                restart: always
                container_name: hyperf01
                networks:
                        - php-net
                ports:
                        - 9505:9501
                command: ["php", "bin/hyperf.php", "start"]
networks:
        php-net:
                external:
                       name: php-net
  • image: Dockerfile 构建出来的镜像名
  • volumes: 挂载目录
    • 开发环境目录结构解释:/www 是项目目录,里面包含多个项目文件夹
    • 此处挂载 /www ,是为了方便只使用一个 php 容器,可以运行多个 hyperf 项目
  • command: 容器启动时的执行命令
    • 一般情况下,我们挂载了指定的项目名称,在容器启动时需要指定启动命令
    • 但是,目前挂载的 /www,没有指定的项目,如果使用 command: ["php", "bin/hyperf.php", "start"] 容器会启动不成功
    • 所以,我们在这里不能使用 command 启动命令,也是为了方便多项目各自启动
  • tty、stdin_open:
    • Dockerfile、docker-compose 中都没有了启动命令、容器启动不成功
    • 所以,追加这两个参数,允许容器可以成功启动,容器不退出
  • ports: 端口映射
    • 为了能直接使用每个项目自己配置的端口
    • 因为为了方便扩展(后续追加更多的项目)
      • 每个项目都有自己的端口号,如果在这里设置,那么每增加一个项目,就需要修改一次端口映射,并重新编排容器,很不方便
      • 所以就取消了这里的端口映射
  • network_mode: 网络模式
    • 固定值:host,容器共享宿主机网络,可以实现不用配置端口号
    • ports 参数不共存
    • 使用了共享网络模式,那么就不再需要配置新的网络了,就取消了 networks 相关的参数配置

执行容器编排

# 后台运行,创建容器并启动
docker-compose up -d

三、启动并运行项目

当我们启动好容器后、进入php容器中

docker exec -it hyperf01 bash

此时可以看到我们的项目目录 /opt/www ,因为这里包含了所有的项目目录,找到你自己的使用 php7.x 的项目,启动即可,启动成功后可以看到端口号就是你再项目中配置的端口号了。

至此,便可以愉快的开启你的牛马生活了 ... ... ...

四、提升

前面,我们提供了一个完整的可直接享用的 Dockerfile 文件,虽然是方便食用,但是问题也随之出现了:

  • 当前是 php7.x 如果我们要换成 php8.x 怎么办呢?对应的 swoole 扩展版本 也需要修改
  • 如果我们需要追加其他扩展怎么办呢?
  • ... ... ...

如果按照当前的模式,每次处理这些问题,都需要去修改 Dockerfile 文件,这样就搞得很麻烦?有没有一个简单有效的办法呢???

有、有、有,,,那就是把这些可变信息,提炼成配置文件,Dockerfile 使用读配置的方式进行,那么我们就不用频繁的修改 Dockerfile 文件了,每次有变动,那就直接修改配置文件,简单直接、方便快速!!!

那么,我现在提供一套可开箱即用的配置方案,至于中间的踩坑过程就直接略过,当然,方案实现不止这么一种,大家可以自行发掘 ... ... ...

目录结构

我们以 project 作为根目录做说明

project
|--config
   |--build.conf
|--logs
|--pecl
|--source
   |--alpine_source.list
   |--debian_source.list
|--build.sh
|--Dockerfile.base
|--Dockerfile.final
  • project:项目根目录
  • project/config:构建配置文件目录
  • project/config/build.conf:构建配置文件,方便修改构建信息
  • project/logs:构建时产生的日志文件目录
  • project/pecl:本地下载扩展目录,方便从本地安装扩展,例如:swoole-5.1.3.tar
  • project/source:自定义 alpine、debian 配置源目录
  • project/source/alpine_source.list:自定义 alpine 系统源配置信息
  • project/source/debian_source.list:自定义 debian 系统源配置信息
  • project/build.sh:构建执行脚本 需要有执行权限
  • project/Dockerfile.base:用于构建基础镜像,只做 apline、debian 系统依赖和工具等更新
  • project/Dockerfile.final:用于构建最终镜像,分阶段:一阶段主要功能 --- 安装扩展;二阶段主要功能 --- 构建最终镜像

文件内容及说明

build.conf
# =============================================================================
# PHP Docker 构建配置文件
# 说明:所有构建参数通过此配置文件控制,无需修改 Dockerfile
# =============================================================================

# -----------------------------------------------------------------------------
# 1. PHP 基础镜像配置
# 说明:取消注释需要使用的版本,只能选择一个
# 支持:php:7.4-cli, php:7.4-alpine, php:8.2-cli, php:8.2-alpine, php:8.3-cli 等
# -----------------------------------------------------------------------------
[base_image]
php:8.2-cli
#php:8.2-alpine
#php:7.4-cli
#php:7.4-alpine
#php:8.3-cli
#php:8.3-alpine

# -----------------------------------------------------------------------------
# 2. PECL 扩展配置
# 说明:通过 pecl install 安装的扩展
# 格式:扩展名-版本号 或 扩展名(自动安装最新兼容版本)
# 示例:redis-5.3.7 或 redis
# -----------------------------------------------------------------------------
[pecl]
#redis-5.3.7
#swoole-4.8.13
#mongodb
#imagick
swoole-5.1.3
redis-6.3.0
#xdebug-3.2.0

# -----------------------------------------------------------------------------
# 3. 本地扩展配置(PHP 内置扩展)
# 说明:通过 docker-php-ext-install 安装
# 常见扩展:pcntl, gd, zip, pdo_mysql, opcache, mysqli, bcmath, sockets 等
# -----------------------------------------------------------------------------
[local]
pcntl
gd
zip
pdo_mysql
mysqli
opcache
bcmath
sockets
#pdo_pgsql
#pdo_sqlsrv
#pdo_oci

# -----------------------------------------------------------------------------
# 4. 数据库扩展配置
# 说明:特殊数据库扩展的安装配置
# 支持:pgsql, sqlsrv, oci8
# 警告:PostgreSQL 扩展会安装大型开发包,可能导致磁盘空间不足
# 建议:如果不需要 PostgreSQL,请注释掉 pgsql 行
# -----------------------------------------------------------------------------
[database]
#pgsql
#sqlsrv
#oci8

# -----------------------------------------------------------------------------
# 5. 系统工具配置
# 说明:需要安装的系统工具
# 常见工具:net-tools, procps, iputils-ping, lsof, ss, htop, vim, nano 等
# -----------------------------------------------------------------------------
[tools]
vim
ss
lsof
iputils-ping
#net-tools
#procps
#htop
#nano
#telnet

# -----------------------------------------------------------------------------
# 6. 构建模式配置
# 说明:prod 生产模式会清理编译工具优化镜像大小,dev 开发模式保留编译工具
# 选项:prod / dev
# -----------------------------------------------------------------------------
[mode]
dev

# -----------------------------------------------------------------------------
# 7. 自定义依赖源配置
# 说明:是否使用自定义的软件包源(国内镜像加速)
# 选项:true / false
# 注意:设置为 true 时,会使用 source 目录下的源配置文件
# -----------------------------------------------------------------------------
[custom_source]
false

# -----------------------------------------------------------------------------
# 8. Composer 镜像源配置
# 说明:Composer 软件包下载源
# 选项:auto(自动,推荐)/ aliyun / tencent / ustc / packagist(官方)
# -----------------------------------------------------------------------------
[composer_mirror]
auto

# -----------------------------------------------------------------------------
# 9. PHP.ini 自定义配置
# 说明:自定义 PHP 运行时配置
# 格式:配置项=值(每行一个配置)
# -----------------------------------------------------------------------------
[php]
swoole.use_shortname=Off
memory_limit=1G
upload_max_filesize=128M
post_max_size=128M
max_execution_time=300
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000

# -----------------------------------------------------------------------------
# 10. 构建目标配置
# 说明:控制构建哪个阶段的镜像
# 选项:base(仅基础镜像)/ final(仅最终镜像)/ all(全部)
# -----------------------------------------------------------------------------
[build_target]
all
#final

# -----------------------------------------------------------------------------
# 11. 强制重建基础镜像配置
# 说明:是否强制重新构建基础镜像(即使已存在)
# 选项:true / false
# -----------------------------------------------------------------------------
[force_rebuild_base]
false
#true

# -----------------------------------------------------------------------------
# 12. 工作目录配置
# 说明:容器内的默认工作目录
# 默认:/var/www/html
# -----------------------------------------------------------------------------
[workdir]
/opt/www

# -----------------------------------------------------------------------------
# 13. 健康检查配置
# 说明:Docker 容器健康检查命令
# 格式:CMD-SHELL 命令
# -----------------------------------------------------------------------------
[healthcheck]
#php -v && php -m

# -----------------------------------------------------------------------------
# 14. 环境变量配置
# 说明:设置容器运行时环境变量
# 格式:KEY=VALUE(每行一个)
# -----------------------------------------------------------------------------
[env]
TZ=Asia/Shanghai
#LANG=C.UTF-8
#APP_ENV=production
#LOG_LEVEL=info
alpine_source.list
# 阿里云源
https://mirrors.aliyun.com/alpine/v3.18/main
https://mirrors.aliyun.com/alpine/v3.18/community

# 腾讯云源
# https://mirrors.cloud.tencent.com/alpine/v3.18/main
# https://mirrors.cloud.tencent.com/alpine/v3.18/community

# 清华源
# https://mirrors.tuna.tsinghua.edu.cn/alpine/v3.18/main
# https://mirrors.tuna.tsinghua.edu.cn/alpine/v3.18/community

# 官方源(备用)
# https://dl-cdn.alpinelinux.org/alpine/v3.18/main
# https://dl-cdn.alpinelinux.org/alpine/v3.18/community

debian_source.list
# 阿里云源
deb http://mirrors.aliyun.com/debian/ bullseye main non-free contrib
deb http://mirrors.aliyun.com/debian-security/ bullseye-security main
deb http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib
deb http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib

# 腾讯云源
# deb http://mirrors.cloud.tencent.com/debian/ bullseye main non-free contrib
# deb http://mirrors.cloud.tencent.com/debian-security/ bullseye-security main
# deb http://mirrors.cloud.tencent.com/debian/ bullseye-updates main non-free contrib
# deb http://mirrors.cloud.tencent.com/debian/ bullseye-backports main non-free contrib

# 清华源
# deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free
# deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free
# deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free
# deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main contrib non-free

# 官方源(备用)
# deb http://deb.debian.org/debian bullseye main contrib non-free
# deb http://deb.debian.org/debian-security/ bullseye-security main contrib non-free
# deb http://deb.debian.org/debian bullseye-updates main contrib non-free

Dockerfile.base
# =============================================================================
# PHP 基础镜像 Dockerfile
# 功能:安装系统依赖、编译工具和常用库,为后续扩展安装做准备
# 说明:此文件不需要手动修改,所有配置通过 build.conf 传入
# =============================================================================

# 构建参数:基础镜像
ARG BASE_IMAGE
FROM ${BASE_IMAGE}

# 构建参数:系统类型和配置
ARG SYSTEM_TYPE
ARG CUSTOM_SOURCE
ARG TOOLS=""
ARG DATABASE_EXTENSIONS=""

# 设置工作目录
WORKDIR /tmp/build

RUN mkdir -p /tmp/pecl

# 设置环境变量防止 OOM 和构建优化
ENV MAKEFLAGS="-j1"
ENV COMPOSER_MEMORY_LIMIT=-1
ENV DEBIAN_FRONTEND=noninteractive

# 复制源配置文件
COPY source/${SYSTEM_TYPE}_source.list /tmp/source.list

# 安装基础依赖和编译工具
RUN set -eux; \
    echo "========================================"; \
    echo "开始构建 PHP 基础镜像"; \
    echo "系统类型: ${SYSTEM_TYPE}"; \
    echo "自定义源: ${CUSTOM_SOURCE}"; \
    echo "系统工具: ${TOOLS}"; \
    echo "数据库扩展: ${DATABASE_EXTENSIONS}"; \
    echo "========================================"; \
    \
    # 定义重试函数
    retry_command() { \
        local max_attempts=3; \
        local attempt=1; \
        local cmd="$@"; \
        while [ $attempt -le $max_attempts ]; do \
            echo "[尝试 ${attempt}/${max_attempts}] 执行: ${cmd}"; \
            if eval "$cmd"; then \
                echo "[成功] 命令执行成功"; \
                return 0; \
            fi; \
            echo "[失败] 命令执行失败,等待 5 秒后重试..."; \
            sleep 5; \
            attempt=$((attempt + 1)); \
        done; \
        echo "[错误] 命令执行失败,已达到最大重试次数"; \
        return 1; \
    }; \
    \
    # Alpine 系统配置
    if [ "$SYSTEM_TYPE" = "alpine" ]; then \
        echo "----------------------------------------"; \
        echo "[步骤 1/10] 配置 Alpine 软件源"; \
        echo "----------------------------------------"; \
        if [ "$CUSTOM_SOURCE" = "true" ]; then \
            echo "使用自定义镜像源"; \
            cat /tmp/source.list > /etc/apk/repositories; \
            cat /etc/apk/repositories; \
        fi; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 2/10] 更新软件包索引"; \
        echo "----------------------------------------"; \
        retry_command "apk update" || exit 1; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 3/10] 安装基础工具"; \
        echo "----------------------------------------"; \
        retry_command "apk add --no-cache bash curl wget git vim unzip" || exit 1; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 4/10] 安装开发库"; \
        echo "----------------------------------------"; \
        retry_command "apk add --no-cache libzip-dev zlib-dev libpng-dev libjpeg-turbo-dev freetype-dev libwebp-dev libxpm-dev openssl-dev curl-dev" || exit 1; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 5/10] 安装 XML 和基础数据库库"; \
        echo "----------------------------------------"; \
        retry_command "apk add --no-cache libxml2-dev oniguruma-dev sqlite-dev" || exit 1; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 6/10] 安装编译工具"; \
        echo "----------------------------------------"; \
        retry_command "apk add --no-cache autoconf make g++ linux-headers" || exit 1; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 7/10] 安装数据库扩展依赖"; \
        echo "----------------------------------------"; \
        if echo "$DATABASE_EXTENSIONS" | grep -q "pgsql"; then \
            echo "安装 PostgreSQL 客户端库..."; \
            retry_command "apk add --no-cache postgresql-dev postgresql-client" || echo "PostgreSQL 依赖安装失败"; \
        fi; \
        if echo "$DATABASE_EXTENSIONS" | grep -q "sqlsrv"; then \
            echo "安装 SQL Server 依赖..."; \
            retry_command "apk add --no-cache unixodbc-dev freetds-dev" || echo "SQL Server 依赖安装失败"; \
        fi; \
        if echo "$DATABASE_EXTENSIONS" | grep -q "oci8"; then \
            echo "Oracle 客户端需要手动配置,跳过自动安装"; \
        fi; \
        \
        if [ -n "$TOOLS" ]; then \
            echo "----------------------------------------"; \
            echo "[步骤 8/10] 安装系统工具"; \
            echo "----------------------------------------"; \
            ALPINE_TOOLS=""; \
            for tool in $TOOLS; do \
                case "$tool" in \
                    net-tools) ALPINE_TOOLS="$ALPINE_TOOLS net-tools" ;; \
                    procps) ALPINE_TOOLS="$ALPINE_TOOLS procps" ;; \
                    iputils-ping) ALPINE_TOOLS="$ALPINE_TOOLS iputils" ;; \
                    vim) ALPINE_TOOLS="$ALPINE_TOOLS vim" ;; \
                    htop) ALPINE_TOOLS="$ALPINE_TOOLS htop" ;; \
                    nano) ALPINE_TOOLS="$ALPINE_TOOLS nano" ;; \
                    telnet) ALPINE_TOOLS="$ALPINE_TOOLS busybox-extras" ;; \
                    lsof) ALPINE_TOOLS="$ALPINE_TOOLS lsof" ;; \
                    ss) ALPINE_TOOLS="$ALPINE_TOOLS iproute2-ss" ;; \
                    *) ALPINE_TOOLS="$ALPINE_TOOLS $tool" ;; \
                esac; \
            done; \
            if [ -n "$ALPINE_TOOLS" ]; then \
                retry_command "apk add --no-cache $ALPINE_TOOLS" || echo "部分工具安装失败"; \
            fi; \
        fi; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 9/10] 配置 GD 扩展依赖"; \
        echo "----------------------------------------"; \
        docker-php-ext-configure gd \
            --with-freetype \
            --with-jpeg \
            --with-webp || echo "GD 配置失败,继续..."; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 10/10] 清理缓存"; \
        echo "----------------------------------------"; \
        rm -rf /var/cache/apk/*; \
    \
    # Debian 系统配置
    else \
        echo "----------------------------------------"; \
        echo "[步骤 1/10] 配置 Debian 软件源"; \
        echo "----------------------------------------"; \
        if [ "$CUSTOM_SOURCE" = "true" ]; then \
            echo "使用自定义镜像源"; \
            cat /tmp/source.list > /etc/apt/sources.list; \
            cat /etc/apt/sources.list; \
        fi; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 2/10] 更新软件包索引"; \
        echo "----------------------------------------"; \
        retry_command "apt-get update" || exit 1; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 3/10] 安装基础工具"; \
        echo "----------------------------------------"; \
        retry_command "apt-get install -y --no-install-recommends curl wget ca-certificates git vim unzip" || exit 1; \
        rm -rf /var/lib/apt/lists/*; apt-get clean; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 4/10] 安装开发库"; \
        echo "----------------------------------------"; \
        apt-get update; \
        retry_command "apt-get install -y --no-install-recommends \
            libzip-dev zlib1g-dev libpng-dev libjpeg-dev \
            libfreetype6-dev libwebp-dev libxpm-dev \
            libssl-dev libcurl4-openssl-dev" || exit 1; \
        rm -rf /var/lib/apt/lists/*; apt-get clean; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 5/10] 安装 XML 和基础数据库库"; \
        echo "----------------------------------------"; \
        apt-get update; \
        retry_command "apt-get install -y --no-install-recommends \
            libxml2-dev libonig-dev libsqlite3-dev" || exit 1; \
        rm -rf /var/lib/apt/lists/*; apt-get clean; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 6/10] 安装编译工具"; \
        echo "----------------------------------------"; \
        apt-get update; \
        retry_command "apt-get install -y --no-install-recommends \
            autoconf g++ make pkg-config" || exit 1; \
        rm -rf /var/lib/apt/lists/*; apt-get clean; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 7/10] 安装数据库扩展依赖"; \
        echo "----------------------------------------"; \
        if echo "$DATABASE_EXTENSIONS" | grep -q "pgsql"; then \
            echo "安装 PostgreSQL 客户端库..."; \
            apt-get update; \
            retry_command "apt-get install -y --no-install-recommends libpq-dev postgresql-client" || echo "PostgreSQL 依赖安装失败"; \
            rm -rf /var/lib/apt/lists/*; apt-get clean; \
        fi; \
        if echo "$DATABASE_EXTENSIONS" | grep -q "sqlsrv"; then \
            echo "安装 SQL Server ODBC 驱动..."; \
            apt-get update; \
            retry_command "apt-get install -y --no-install-recommends \
                apt-transport-https gnupg2 unixodbc-dev" || echo "基础依赖安装失败"; \
            curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - || echo "添加 MS 密钥失败"; \
            curl https://packages.microsoft.com/config/debian/11/prod.list > /etc/apt/sources.list.d/mssql-release.list || echo "添加 MS 源失败"; \
            apt-get update; \
            ACCEPT_EULA=Y retry_command "apt-get install -y --no-install-recommends msodbcsql18" || echo "ODBC 驱动安装失败"; \
            ACCEPT_EULA=Y retry_command "apt-get install -y --no-install-recommends mssql-tools18" || echo "SQL Server 工具安装失败"; \
            rm -rf /var/lib/apt/lists/*; apt-get clean; \
        fi; \
        if echo "$DATABASE_EXTENSIONS" | grep -q "oci8"; then \
            echo "Oracle Instant Client 需要手动下载和配置"; \
            echo "请参考: https://www.oracle.com/database/technologies/instant-client.html"; \
        fi; \
        \
        if [ -n "$TOOLS" ]; then \
            echo "----------------------------------------"; \
            echo "[步骤 8/10] 安装系统工具"; \
            echo "----------------------------------------"; \
            apt-get update; \
            # 将工具名称映射到正确的 Debian 包名
            DEBIAN_TOOLS=""; \
            for tool in $TOOLS; do \
                case "$tool" in \
                    ss) DEBIAN_TOOLS="$DEBIAN_TOOLS iproute2" ;; \
                    lsof) DEBIAN_TOOLS="$DEBIAN_TOOLS lsof" ;; \
                    *) DEBIAN_TOOLS="$DEBIAN_TOOLS $tool" ;; \
                esac; \
            done; \
            retry_command "apt-get install -y --no-install-recommends $DEBIAN_TOOLS" || echo "部分工具安装失败"; \
            rm -rf /var/lib/apt/lists/*; apt-get clean; \
        fi; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 9/10] 配置 GD 扩展依赖"; \
        echo "----------------------------------------"; \
        docker-php-ext-configure gd \
            --with-freetype \
            --with-jpeg \
            --with-webp || echo "GD 配置失败,继续..."; \
        \
        echo "----------------------------------------"; \
        echo "[步骤 10/10] 清理缓存"; \
        echo "----------------------------------------"; \
        rm -rf /var/lib/apt/lists/*; \
        apt-get clean; \
    fi; \
    \
    # 清理临时文件
    rm -rf /tmp/source.list; \
    \
    echo "========================================"; \
    echo "基础镜像构建完成"; \
    echo "========================================";

# 健康检查
RUN php -v && php -m

# 设置默认工作目录
WORKDIR /var/www/html
  • 该文件注释已标注在文档中

  • 简要说明

    • 执行该文件,会生成类似于 php-base:8.2-debian php-base:8.2-alpine 的基础镜像
    • 基础镜像的生成规则
      • 镜像名称规则:固定值 php-base

      • tag 标签生成规则:自动读取配置文件中的 基础镜像名,获取版本号 和 对应的系统信息(debian/alpine)作为 tag 名称

Dockerfile.final
# =============================================================================
# PHP 最终镜像 Dockerfile(多阶段构建)
# 功能:基于基础镜像安装 PHP 扩展、配置 PHP 和 Composer
# 说明:此文件不需要手动修改,所有配置通过 build.conf 传入
# =============================================================================

# 构建参数:基础镜像名称
ARG BASE_IMAGE_NAME
FROM ${BASE_IMAGE_NAME}

# 扩展和配置参数
ARG SYSTEM_TYPE
ARG PECL_EXTENSIONS=""
ARG LOCAL_EXTENSIONS=""
ARG DATABASE_EXTENSIONS=""
ARG PHP_INI_CONFIGS=""
ARG ENV_VARS=""
ARG COMPOSER_MIRROR="auto"
ARG BUILD_MODE="dev"
ARG WORKDIR="/var/www/html"

# 设置工作目录
WORKDIR /tmp/build

RUN mkdir -p /tmp/pecl /host/pecl

COPY pecl /tmp/pecl-source

RUN set -eux; \
    if [ -d "/tmp/pecl-source" ] && [ "$(ls -A /tmp/pecl-source/*.tgz 2>/dev/null)" ]; then \
        echo "[缓存] 发现本地 PECL 扩展包"; \
        cp /tmp/pecl-source/*.tgz /tmp/pecl/ 2>/dev/null || true; \
        ls -lh /tmp/pecl/; \
    else \
        echo "[信息] 未发现本地 PECL 扩展缓存,将从远程下载"; \
    fi; \
    rm -rf /tmp/pecl-source;

# 复制本地扩展目录(使用通配符,不存在时自动忽略)
# 已经通过上面的 RUN 命令处理,不再需要

# 安装数据库扩展
RUN set -eux; \
    echo "========================================"; \
    echo "开始安装数据库扩展"; \
    echo "系统类型: ${SYSTEM_TYPE}"; \
    echo "数据库扩展: ${DATABASE_EXTENSIONS}"; \
    echo "========================================"; \
    \
    # 定义重试函数
    retry_command() { \
        local max_attempts=3; \
        local attempt=1; \
        local cmd="$@"; \
        while [ $attempt -le $max_attempts ]; do \
            echo "[尝试 ${attempt}/${max_attempts}] 执行: ${cmd}"; \
            if eval "$cmd"; then \
                echo "[成功] 命令执行成功"; \
                return 0; \
            fi; \
            echo "[失败] 命令执行失败,等待 5 秒后重试..."; \
            sleep 5; \
            attempt=$((attempt + 1)); \
        done; \
        echo "[错误] 命令执行失败,已达到最大重试次数"; \
        return 1; \
    }; \
    \
    # 安装数据库扩展
    if [ -n "$DATABASE_EXTENSIONS" ]; then \
        for db_ext in $DATABASE_EXTENSIONS; do \
            case "$db_ext" in \
                pgsql) \
                    echo "========================================"; \
                    echo "[数据库] PostgreSQL 扩展安装"; \
                    echo "========================================"; \
                    if [ "$SYSTEM_TYPE" = "alpine" ]; then \
                        echo "[步骤 1/3] 安装 PostgreSQL 运行时库..."; \
                        apk add --no-cache postgresql-client libpq; \
                        rm -rf /var/cache/apk/*; \
                    else \
                        echo "[步骤 1/3] 安装 PostgreSQL 运行时库..."; \
                        apt-get update; \
                        apt-get install -y --no-install-recommends libpq5 postgresql-client; \
                        rm -rf /var/lib/apt/lists/*; apt-get clean; \
                    fi; \
                    echo "[步骤 2/3] 编译安装 pdo_pgsql 和 pgsql 扩展..."; \
                    docker-php-ext-install -j1 pdo_pgsql pgsql; \
                    echo "[步骤 3/3] 验证扩展安装..."; \
                    php -m | grep -i pgsql || { echo "[错误] PostgreSQL 扩展安装失败"; exit 1; }; \
                    echo "[成功] PostgreSQL 扩展安装完成"; \
                    ;; \
                sqlsrv|pdo_sqlsrv) \
                    echo "========================================"; \
                    echo "[数据库] SQL Server 扩展安装"; \
                    echo "========================================"; \
                    if [ "$SYSTEM_TYPE" = "alpine" ]; then \
                        echo "[警告] Alpine 系统暂不支持 SQL Server 扩展,跳过..."; \
                    else \
                        echo "[步骤 1/5] 安装 ODBC 依赖..."; \
                        apt-get update; \
                        apt-get install -y --no-install-recommends \
                            apt-transport-https \
                            gnupg2 \
                            unixodbc-dev; \
                        rm -rf /var/lib/apt/lists/*; apt-get clean; \
                        \
                        echo "[步骤 2/5] 添加 Microsoft 软件源..."; \
                        curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-archive-keyring.gpg; \
                        echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/debian/11/prod bullseye main" > /etc/apt/sources.list.d/mssql-release.list; \
                        \
                        echo "[步骤 3/5] 安装 ODBC 驱动..."; \
                        apt-get update; \
                        ACCEPT_EULA=Y apt-get install -y --no-install-recommends msodbcsql18 mssql-tools18; \
                        echo 'export PATH="$PATH:/opt/mssql-tools18/bin"' >> ~/.bashrc; \
                        rm -rf /var/lib/apt/lists/*; apt-get clean; \
                        \
                        echo "[步骤 4/5] 安装 SQL Server PHP 扩展..."; \
                        pecl install sqlsrv pdo_sqlsrv; \
                        docker-php-ext-enable sqlsrv pdo_sqlsrv; \
                        \
                        echo "[步骤 5/5] 验证扩展安装..."; \
                        php -m | grep -i sqlsrv || { echo "[错误] SQL Server 扩展安装失败"; exit 1; }; \
                        echo "[成功] SQL Server 扩展安装完成"; \
                        \
                        echo "[配置] 设置 ODBC 驱动路径..."; \
                        echo "" >> /usr/local/etc/php/conf.d/sqlsrv.ini; \
                        echo "; SQL Server 扩展配置" >> /usr/local/etc/php/conf.d/sqlsrv.ini; \
                        echo "sqlsrv.WarningsReturnAsErrors = 0" >> /usr/local/etc/php/conf.d/sqlsrv.ini; \
                    fi; \
                    ;; \
                oci8|pdo_oci) \
                    echo "========================================"; \
                    echo "[数据库] Oracle 扩展安装"; \
                    echo "========================================"; \
                    echo "[提示] Oracle Instant Client 需要手动下载和配置"; \
                    echo "[步骤 1] 下载 Oracle Instant Client:"; \
                    echo "        https://www.oracle.com/database/technologies/instant-client/downloads.html"; \
                    echo "[步骤 2] 将文件放置在 /opt/oracle 目录"; \
                    echo "[步骤 3] 设置环境变量:"; \
                    echo "        export LD_LIBRARY_PATH=/opt/oracle/instantclient_21_1:\$LD_LIBRARY_PATH"; \
                    echo "[步骤 4] 安装扩展:"; \
                    echo "        pecl install oci8"; \
                    echo "        docker-php-ext-enable oci8"; \
                    echo "[跳过] 需要手动配置 Oracle 客户端"; \
                    ;; \
            esac; \
        done; \
        echo "[成功] 所有数据库扩展处理完成"; \
    else \
        echo "[信息] 未配置数据库扩展"; \
    fi; \
    \
    # 清理临时文件
    rm -rf /tmp/pear; \
    if [ "$SYSTEM_TYPE" = "alpine" ]; then \
        rm -rf /var/cache/apk/*; \
    else \
        rm -rf /var/lib/apt/lists/*; \
        apt-get clean; \
    fi;

# 安装 PHP 扩展
RUN set -eux; \
    echo "========================================"; \
    echo "开始安装 PHP 扩展"; \
    echo "系统类型: ${SYSTEM_TYPE}"; \
    echo "LOCAL 扩展: ${LOCAL_EXTENSIONS}"; \
    echo "PECL 扩展: ${PECL_EXTENSIONS}"; \
    echo "========================================"; \
    \
    cd /tmp || exit 1; \
    \
    # 定义重试函数
    retry_command() { \
        local max_attempts=3; \
        local attempt=1; \
        local cmd="$@"; \
        while [ $attempt -le $max_attempts ]; do \
            echo "[尝试 ${attempt}/${max_attempts}] 执行: ${cmd}"; \
            if eval "$cmd"; then \
                echo "[成功] 命令执行成功"; \
                return 0; \
            fi; \
            echo "[失败] 命令执行失败,等待 5 秒后重试..."; \
            sleep 5; \
            attempt=$((attempt + 1)); \
        done; \
        echo "[错误] 命令执行失败,已达到最大重试次数"; \
        return 1; \
    }; \
    \
    if [ -n "$LOCAL_EXTENSIONS" ]; then \
        echo "----------------------------------------"; \
        echo "安装 LOCAL 扩展: $LOCAL_EXTENSIONS"; \
        echo "----------------------------------------"; \
        \
        for ext in $(echo "$LOCAL_EXTENSIONS" | tr ',' ' '); do \
            [ -z "$ext" ] && continue; \
            case "$ext" in \
                gd) echo "[扩展] GD - 图像处理库" ;; \
                zip) \
                    echo "[扩展] ZIP - 压缩文件支持"; \
                    if [ "$SYSTEM_TYPE" = "alpine" ]; then \
                        apk add --no-cache libzip-dev; \
                        rm -rf /var/cache/apk/*; \
                    fi; \
                    ;; \
                pdo_mysql) echo "[扩展] PDO_MYSQL - MySQL PDO 驱动" ;; \
                mysqli) echo "[扩展] MYSQLI - MySQL 改进扩展" ;; \
                opcache) echo "[扩展] OPCACHE - 操作码缓存" ;; \
                pcntl) echo "[扩展] PCNTL - 进程控制" ;; \
                bcmath) echo "[扩展] BCMATH - 高精度数学运算" ;; \
                sockets) echo "[扩展] SOCKETS - Socket 通信" ;; \
                *) echo "[扩展] $ext" ;; \
            esac; \
        done; \
        \
        echo "开始批量安装 LOCAL 扩展(单核编译)..."; \
        if ! retry_command "docker-php-ext-install -j1 $LOCAL_EXTENSIONS"; then \
            echo "[错误] LOCAL 扩展安装失败"; \
            exit 1; \
        fi; \
        echo "[成功] LOCAL 扩展安装成功"; \
        \
        rm -rf /tmp/pear /tmp/*.tgz; \
        if [ "$SYSTEM_TYPE" = "alpine" ]; then \
            rm -rf /var/cache/apk/*; \
        else \
            rm -rf /var/lib/apt/lists/*; \
            apt-get clean; \
        fi; \
    fi; \
    \
    mkdir -p /tmp/pecl-work; \
    \
    if [ -n "$PECL_EXTENSIONS" ]; then \
        echo "----------------------------------------"; \
        echo "安装 PECL 扩展: $PECL_EXTENSIONS"; \
        echo "----------------------------------------"; \
        \
        for ext in $(echo "$PECL_EXTENSIONS" | tr ',' ' '); do \
            [ -z "$ext" ] && continue; \
            ext_name=$(echo "$ext" | cut -d'-' -f1); \
            ext_version=$(echo "$ext" | grep -oP '\-\K.*' || echo ""); \
            \
            echo "========================================"; \
            echo "[处理] PECL 扩展: $ext_name"; \
            if [ -n "$ext_version" ]; then \
                echo "[版本] $ext_version"; \
            else \
                echo "[版本] 最新稳定版"; \
            fi; \
            echo "========================================"; \
            \
            cd /tmp/pecl-work || exit 1; \
            rm -rf /tmp/pecl-work/*; \
            \
            local_ext_file="/tmp/pecl/${ext}.tgz"; \
            host_ext_file="/host/pecl/${ext}.tgz"; \
            ext_exists=false; \
            \
            if [ -f "$local_ext_file" ]; then \
                echo "[缓存] 发现本地扩展文件: ${ext}.tgz"; \
                ext_exists=true; \
            fi; \
            \
            # 根据扩展类型配置编译选项
            case "$ext_name" in \
                swoole) \
                    echo "[配置] Swoole 扩展 - 启用完整功能支持"; \
                    configure_opts='enable-openssl="yes" enable-sockets="yes" enable-http2="yes" enable-mysqlnd="yes" enable-swoole-curl="yes"'; \
                    \
                    if [ "$ext_exists" = true ]; then \
                        echo "[安装] 使用本地缓存安装"; \
                        if ! retry_command "pecl install --configureoptions '$configure_opts' $local_ext_file"; then \
                            echo "[警告] 本地安装失败,尝试 PECL 仓库"; \
                            ext_exists=false; \
                        fi; \
                    fi; \
                    \
                    if [ "$ext_exists" = false ]; then \
                        echo "[下载] 从 PECL 仓库下载"; \
                        if ! retry_command "pecl download $ext"; then \
                            echo "[错误] 扩展下载失败"; \
                            exit 1; \
                        fi; \
                        downloaded_file=$(ls /tmp/pecl-work/${ext_name}-*.tgz 2>/dev/null | head -n 1); \
                        if [ -f "$downloaded_file" ]; then \
                            echo "[保存] 保存扩展到 pecl 目录: ${ext}.tgz"; \
                            cp "$downloaded_file" "$host_ext_file" 2>/dev/null || echo "[警告] 无法保存到宿主机"; \
                            cp "$downloaded_file" "$local_ext_file"; \
                            echo "[安装] 安装扩展"; \
                            if ! retry_command "pecl install --configureoptions '$configure_opts' $downloaded_file"; then \
                                echo "[错误] 扩展安装失败"; \
                                exit 1; \
                            fi; \
                        fi; \
                    fi; \
                    ;; \
                redis) \
                    echo "[配置] Redis 扩展 - 启用 LZF 压缩"; \
                    configure_opts='enable-redis-igbinary="no" enable-redis-msgpack="no" enable-redis-lzf="yes" enable-redis-zstd="no"'; \
                    \
                    if [ "$ext_exists" = true ]; then \
                        echo "[安装] 使用本地缓存安装"; \
                        if ! retry_command "pecl install --configureoptions '$configure_opts' $local_ext_file"; then \
                            echo "[警告] 本地安装失败,尝试 PECL 仓库"; \
                            ext_exists=false; \
                        fi; \
                    fi; \
                    \
                    if [ "$ext_exists" = false ]; then \
                        echo "[下载] 从 PECL 仓库下载"; \
                        if ! retry_command "pecl download $ext"; then \
                            echo "[错误] 扩展下载失败"; \
                            exit 1; \
                        fi; \
                        downloaded_file=$(ls /tmp/pecl-work/${ext_name}-*.tgz 2>/dev/null | head -n 1); \
                        if [ -f "$downloaded_file" ]; then \
                            echo "[保存] 保存扩展到 pecl 目录: ${ext}.tgz"; \
                            cp "$downloaded_file" "$host_ext_file" 2>/dev/null || echo "[警告] 无法保存到宿主机"; \
                            cp "$downloaded_file" "$local_ext_file"; \
                            echo "[安装] 安装扩展"; \
                            if ! retry_command "pecl install --configureoptions '$configure_opts' $downloaded_file"; then \
                                echo "[错误] 扩展安装失败"; \
                                exit 1; \
                            fi; \
                        fi; \
                    fi; \
                    ;; \
                mongodb) \
                    echo "[配置] MongoDB 扩展 - 使用系统 OpenSSL"; \
                    configure_opts='enable-mongodb-ssl="yes"'; \
                    \
                    if [ "$ext_exists" = true ]; then \
                        echo "[安装] 使用本地缓存安装"; \
                        if ! retry_command "pecl install --configureoptions '$configure_opts' $local_ext_file"; then \
                            echo "[警告] 本地安装失败,尝试 PECL 仓库"; \
                            ext_exists=false; \
                        fi; \
                    fi; \
                    \
                    if [ "$ext_exists" = false ]; then \
                        echo "[下载] 从 PECL 仓库下载"; \
                        if ! retry_command "pecl download $ext"; then \
                            echo "[错误] 扩展下载失败"; \
                            exit 1; \
                        fi; \
                        downloaded_file=$(ls /tmp/pecl-work/${ext_name}-*.tgz 2>/dev/null | head -n 1); \
                        if [ -f "$downloaded_file" ]; then \
                            echo "[保存] 保存扩展到 pecl 目录: ${ext}.tgz"; \
                            cp "$downloaded_file" "$host_ext_file" 2>/dev/null || echo "[警告] 无法保存到宿主机"; \
                            cp "$downloaded_file" "$local_ext_file"; \
                            echo "[安装] 安装扩展"; \
                            if ! retry_command "pecl install --configureoptions '$configure_opts' $downloaded_file"; then \
                                echo "[错误] 扩展安装失败"; \
                                exit 1; \
                            fi; \
                        fi; \
                    fi; \
                    ;; \
                *) \
                    echo "[配置] $ext_name 扩展 - 使用默认配置"; \
                    \
                    if [ "$ext_exists" = true ]; then \
                        echo "[安装] 使用本地缓存安装"; \
                        if ! retry_command "pecl install $local_ext_file"; then \
                            echo "[警告] 本地安装失败,尝试 PECL 仓库"; \
                            ext_exists=false; \
                        fi; \
                    fi; \
                    \
                    if [ "$ext_exists" = false ]; then \
                        echo "[下载] 从 PECL 仓库下载"; \
                        if ! retry_command "pecl download $ext"; then \
                            echo "[错误] 扩展下载失败"; \
                            exit 1; \
                        fi; \
                        downloaded_file=$(ls /tmp/pecl-work/${ext_name}-*.tgz 2>/dev/null | head -n 1); \
                        if [ -f "$downloaded_file" ]; then \
                            echo "[保存] 保存扩展到 pecl 目录: ${ext}.tgz"; \
                            cp "$downloaded_file" "$host_ext_file" 2>/dev/null || echo "[警告] 无法保存到宿主机"; \
                            cp "$downloaded_file" "$local_ext_file"; \
                            echo "[安装] 安装扩展"; \
                            if ! retry_command "pecl install $downloaded_file"; then \
                                echo "[错误] 扩展安装失败"; \
                                exit 1; \
                            fi; \
                        fi; \
                    fi; \
                    ;; \
            esac; \
            \
            echo "[启用] 启用 $ext_name 扩展"; \
            docker-php-ext-enable "$ext_name" || echo "[警告] 无法启用 $ext_name,可能已经启用"; \
            \
            echo "[验证] 验证扩展安装"; \
            if php -m | grep -i "$ext_name"; then \
                echo "[成功] $ext_name 扩展安装并启用成功"; \
            else \
                echo "[警告] $ext_name 扩展可能未正确加载"; \
            fi; \
            \
            echo "========================================"; \
        done; \
        \
        echo "[成功] 所有 PECL 扩展安装完成"; \
        \
        cd /tmp || exit 1; \
        rm -rf /tmp/pecl-work; \
        rm -rf /tmp/pear /tmp/*.tgz; \
        if [ "$SYSTEM_TYPE" = "alpine" ]; then \
            rm -rf /var/cache/apk/*; \
        else \
            rm -rf /var/lib/apt/lists/*; \
            apt-get clean; \
        fi; \
    fi; \
    \
    # 最终验证
    echo "========================================"; \
    echo "PHP 扩展安装完成"; \
    echo "已安装的扩展列表:"; \
    php -m; \
    echo "========================================";

# 配置 PHP.ini
RUN set -eux; \
    if [ -n "$PHP_INI_CONFIGS" ]; then \
        echo "========================================"; \
        echo "配置 PHP.ini"; \
        echo "========================================"; \
        \
        echo "$PHP_INI_CONFIGS" | tr '|' '\n' | while read -r config; do \
            [ -z "$config" ] && continue; \
            echo "[配置] $config"; \
            echo "$config" >> /usr/local/etc/php/conf.d/custom.ini; \
        done; \
        \
        echo "----------------------------------------"; \
        echo "PHP.ini 配置内容:"; \
        cat /usr/local/etc/php/conf.d/custom.ini; \
        echo "========================================"; \
    fi;

# 设置环境变量
RUN set -eux; \
    if [ -n "$ENV_VARS" ]; then \
        echo "========================================"; \
        echo "配置环境变量"; \
        echo "========================================"; \
        \
        echo "$ENV_VARS" | tr '|' '\n' | while read -r env_var; do \
            [ -z "$env_var" ] && continue; \
            echo "[环境变量] $env_var"; \
            echo "export $env_var" >> /etc/profile; \
        done; \
        \
        echo "[成功] 环境变量配置完成"; \
    fi;

# 安装 Composer
RUN set -eux; \
    echo "========================================"; \
    echo "安装 Composer"; \
    echo "Composer 镜像: ${COMPOSER_MIRROR}"; \
    echo "========================================"; \
    \
    retry_command() { \
        local max_attempts=3; \
        local attempt=1; \
        local cmd="$@"; \
        while [ $attempt -le $max_attempts ]; do \
            echo "[尝试 ${attempt}/${max_attempts}]"; \
            if eval "$cmd"; then \
                return 0; \
            fi; \
            sleep 5; \
            attempt=$((attempt + 1)); \
        done; \
        return 1; \
    }; \
    \
    retry_command "curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer"; \
    \
    case "$COMPOSER_MIRROR" in \
        aliyun) \
            echo "[配置] 阿里云 Composer 镜像"; \
            composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/; \
            ;; \
        tencent) \
            echo "[配置] 腾讯云 Composer 镜像"; \
            composer config -g repo.packagist composer https://mirrors.cloud.tencent.com/composer/; \
            ;; \
        ustc) \
            echo "[配置] 中科大 Composer 镜像"; \
            composer config -g repo.packagist composer https://packagist.mirrors.ustc.edu.cn/; \
            ;; \
        packagist) \
            echo "[配置] 官方 Packagist 源"; \
            ;; \
        auto) \
            echo "[配置] 自动检测,使用阿里云镜像"; \
            composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/; \
            ;; \
        *) \
            echo "[警告] 未知的 Composer 镜像配置,使用默认源"; \
            ;; \
    esac; \
    \
    composer --version; \
    echo "[成功] Composer 安装完成";

# 清理和优化
RUN set -eux; \
    echo "========================================"; \
    echo "清理临时文件和优化镜像"; \
    echo "构建模式: ${BUILD_MODE}"; \
    echo "========================================"; \
    \
    rm -rf /tmp/pear; \
    rm -rf /tmp/pecl; \
    \
    if [ "$BUILD_MODE" = "prod" ]; then \
        echo "[模式] 生产模式 - 清理编译工具以优化镜像大小"; \
        if [ "$SYSTEM_TYPE" = "alpine" ]; then \
            apk del autoconf g++ make linux-headers; \
            rm -rf /var/cache/apk/*; \
        else \
            apt-get purge -y autoconf g++ make pkg-config; \
            apt-get autoremove -y; \
            rm -rf /var/lib/apt/lists/*; \
            apt-get clean; \
        fi; \
    else \
        echo "[模式] 开发模式 - 保留编译工具"; \
        if [ "$SYSTEM_TYPE" = "alpine" ]; then \
            rm -rf /var/cache/apk/*; \
        else \
            rm -rf /var/lib/apt/lists/*; \
            apt-get clean; \
        fi; \
    fi; \
    \
    rm -rf /tmp/*; \
    rm -rf /var/tmp/*; \
    \
    echo "[成功] 清理完成";

# 最终验证
RUN set -eux; \
    echo "========================================"; \
    echo "最终验证"; \
    echo "========================================"; \
    echo "PHP 版本:"; \
    php -v; \
    echo ""; \
    echo "已安装的扩展:"; \
    php -m; \
    echo ""; \
    echo "Composer 版本:"; \
    composer --version; \
    echo "========================================"; \
    echo "✅ 镜像构建完成!"; \
    echo "========================================";

# 设置工作目录
ARG WORKDIR
WORKDIR ${WORKDIR}
  • 该文件注释已标注在文档中
  • 简要说明
    • 执行该文件,会生成你指定的镜像名称和 tag 标签
      • 使用多阶段构建模式,第一阶段: 安装 pecl 和 local 扩展;第二阶段: 构建最终镜像
build.sh
#!/bin/bash

# =============================================================================
# PHP Docker 多阶段构建脚本
# 功能:根据配置文件自动构建 PHP 基础镜像和最终镜像
# 用法:./build.sh [最终镜像名称]
# 版本:2.0
# =============================================================================

set -e  # 遇到错误立即退出

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
PURPLE='\033[0;35m'
NC='\033[0m' # 无颜色

# 配置文件路径
CONFIG_FILE="./config/build.conf"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_LOG_FILE="./logs/build_$(date '+%Y%m%d_%H%M%S').log"

# 日志函数
log_info() {
    local msg="[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $1"
    echo -e "${BLUE}${msg}${NC}"
    echo "$msg" >> "$BUILD_LOG_FILE"
}

log_success() {
    local msg="[SUCCESS] $(date '+%Y-%m-%d %H:%M:%S') - $1"
    echo -e "${GREEN}${msg}${NC}"
    echo "$msg" >> "$BUILD_LOG_FILE"
}

log_warn() {
    local msg="[WARN] $(date '+%Y-%m-%d %H:%M:%S') - $1"
    echo -e "${YELLOW}${msg}${NC}"
    echo "$msg" >> "$BUILD_LOG_FILE"
}

log_error() {
    local msg="[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $1"
    echo -e "${RED}${msg}${NC}"
    echo "$msg" >> "$BUILD_LOG_FILE"
}

log_step() {
    local msg="[STEP] $(date '+%Y-%m-%d %H:%M:%S') - $1"
    echo -e "${CYAN}${msg}${NC}"
    echo "$msg" >> "$BUILD_LOG_FILE"
}

# 显示横幅
show_banner() {
    echo -e "${PURPLE}"
    echo "========================================================"
    echo "    PHP Docker 多阶段构建系统 v2.0"
    echo "    Build Time: $(date '+%Y-%m-%d %H:%M:%S')"
    echo "========================================================"
    echo -e "${NC}"
}

# 检查配置文件是否存在
check_config_file() {
    log_step "检查配置文件"
    if [ ! -f "$CONFIG_FILE" ]; then
        log_error "配置文件不存在: $CONFIG_FILE"
        exit 1
    fi
    log_success "配置文件检查通过: $CONFIG_FILE"
}

# 解析配置文件
parse_config() {
    local section=""
    
    log_step "开始解析配置文件"

    while IFS= read -r line || [ -n "$line" ]; do
        # 跳过空行和注释
        [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue

        # 检测 section
        if [[ "$line" =~ ^\[(.+)\]$ ]]; then
            section="${BASH_REMATCH[1]}"
            continue
        fi

        # 读取配置值
        if [ -n "$section" ]; then
            case "$section" in
                base_image)
                    BASE_IMAGE="$line"
                    ;;
                pecl)
                    PECL_EXTENSIONS+=("$line")
                    ;;
                local)
                    LOCAL_EXTENSIONS+=("$line")
                    ;;
                database)
                    DATABASE_EXTENSIONS+=("$line")
                    ;;
                tools)
                    TOOLS+=("$line")
                    ;;
                mode)
                    BUILD_MODE="$line"
                    ;;
                custom_source)
                    CUSTOM_SOURCE="$line"
                    ;;
                composer_mirror)
                    COMPOSER_MIRROR="$line"
                    ;;
                php)
                    PHP_INI_CONFIGS+=("$line")
                    ;;
                build_target)
                    BUILD_TARGET="$line"
                    ;;
                force_rebuild_base)
                    FORCE_REBUILD_BASE="$line"
                    ;;
                workdir)
                    WORKDIR="$line"
                    ;;
                healthcheck)
                    HEALTHCHECK="$line"
                    ;;
                env)
                    ENV_VARS+=("$line")
                    ;;
            esac
        fi
    done < "$CONFIG_FILE"
    
    log_success "配置文件解析完成"
}

# 检测系统类型(Alpine 或 Debian)
detect_system_type() {
    log_step "检测系统类型"
    if [[ "$BASE_IMAGE" == *"alpine"* ]]; then
        SYSTEM_TYPE="alpine"
    else
        SYSTEM_TYPE="debian"
    fi
    log_info "系统类型: $SYSTEM_TYPE"
}

# 生成基础镜像名称
generate_base_image_name() {
    log_step "生成基础镜像名称"
    local version=$(echo "$BASE_IMAGE" | grep -oP 'php:\K[0-9]+\.[0-9]+')
    BASE_IMAGE_NAME="php-base:${version}-${SYSTEM_TYPE}"
    log_info "基础镜像名称: $BASE_IMAGE_NAME"
}

# 检查基础镜像是否存在
check_base_image_exists() {
    log_step "检查基础镜像是否存在"
    if docker image inspect "$BASE_IMAGE_NAME" >/dev/null 2>&1; then
        log_success "基础镜像已存在: $BASE_IMAGE_NAME"
        return 0
    else
        log_warn "基础镜像不存在,需要构建"
        return 1
    fi
}

# 显示配置摘要
show_config_summary() {
    echo -e "${CYAN}"
    echo "========================================================"
    echo "                   构建配置摘要"
    echo "========================================================"
    echo -e "${NC}"
    echo "  基础镜像:         $BASE_IMAGE"
    echo "  系统类型:         $SYSTEM_TYPE"
    echo "  基础镜像名称:     $BASE_IMAGE_NAME"
    echo "  PECL 扩展数量:    ${#PECL_EXTENSIONS[@]}"
    echo "  LOCAL 扩展数量:   ${#LOCAL_EXTENSIONS[@]}"
    echo "  数据库扩展数量:   ${#DATABASE_EXTENSIONS[@]}"
    echo "  系统工具数量:     ${#TOOLS[@]}"
    echo "  构建模式:         $BUILD_MODE"
    echo "  自定义源:         $CUSTOM_SOURCE"
    echo "  Composer 镜像:    $COMPOSER_MIRROR"
    echo "  工作目录:         $WORKDIR"
    echo "  构建目标:         $BUILD_TARGET"
    echo "  强制重建基础:     $FORCE_REBUILD_BASE"
    echo "  日志文件:         $BUILD_LOG_FILE"
    echo -e "${CYAN}"
    echo "========================================================"
    echo -e "${NC}"
}

# 构建基础镜像
build_base_image() {
    log_step "开始构建基础镜像"
    
    # 准备工具参数
    local tools_arg=""
    if [ ${#TOOLS[@]} -gt 0 ]; then
        tools_arg=$(IFS=' '; echo "${TOOLS[*]}")
    fi

    local database_extensions_arg=""
    if [ ${#DATABASE_EXTENSIONS[@]} -gt 0 ]; then
        database_extensions_arg=$(IFS=' '; echo "${DATABASE_EXTENSIONS[*]}")
    fi

    echo -e "${YELLOW}正在构建基础镜像,请稍候...${NC}"
    
    # 构建基础镜像
    docker build \
        --file Dockerfile.base \
        --build-arg BASE_IMAGE="$BASE_IMAGE" \
        --build-arg SYSTEM_TYPE="$SYSTEM_TYPE" \
        --build-arg CUSTOM_SOURCE="$CUSTOM_SOURCE" \
        --build-arg TOOLS="$tools_arg" \
        --build-arg DATABASE_EXTENSIONS="$database_extensions_arg" \
        --tag "$BASE_IMAGE_NAME" \
        --progress=plain \
        . 2>&1 | tee -a "$BUILD_LOG_FILE"

    if [ ${PIPESTATUS[0]} -eq 0 ]; then
        log_success "基础镜像构建成功: $BASE_IMAGE_NAME"
    else
        log_error "基础镜像构建失败,请查看日志: $BUILD_LOG_FILE"
        exit 1
    fi
}

# 构建最终镜像
build_final_image() {
    local final_image_name="$1"

    if [ -z "$final_image_name" ]; then
        log_error "请提供最终镜像名称"
        echo "用法: ./build.sh [最终镜像名称]"
        exit 1
    fi

    log_step "开始构建最终镜像: $final_image_name"

    # 准备 PECL 扩展参数
    local pecl_extensions_arg=""
    if [ ${#PECL_EXTENSIONS[@]} -gt 0 ]; then
        pecl_extensions_arg=$(IFS=,; echo "${PECL_EXTENSIONS[*]}")
    fi

    # 准备 LOCAL 扩展参数
    local local_extensions_arg=""
    if [ ${#LOCAL_EXTENSIONS[@]} -gt 0 ]; then
        local_extensions_arg=$(IFS=' '; echo "${LOCAL_EXTENSIONS[*]}")
    fi

    local database_extensions_arg=""
    if [ ${#DATABASE_EXTENSIONS[@]} -gt 0 ]; then
        database_extensions_arg=$(IFS=' '; echo "${DATABASE_EXTENSIONS[*]}")
    fi

    # 准备 PHP 配置参数
    local php_ini_arg=""
    if [ ${#PHP_INI_CONFIGS[@]} -gt 0 ]; then
        php_ini_arg=$(IFS='|'; echo "${PHP_INI_CONFIGS[*]}")
    fi

    # 准备环境变量参数
    local env_vars_arg=""
    if [ ${#ENV_VARS[@]} -gt 0 ]; then
        env_vars_arg=$(IFS='|'; echo "${ENV_VARS[*]}")
    fi

    mkdir -p ./pecl
    log_info "pecl 目录已准备: ./pecl"

    echo -e "${YELLOW}正在构建最终镜像,请稍候...${NC}"

    # 构建最终镜像
    docker build \
        --file Dockerfile.final \
        --build-arg BASE_IMAGE_NAME="$BASE_IMAGE_NAME" \
        --build-arg SYSTEM_TYPE="$SYSTEM_TYPE" \
        --build-arg PECL_EXTENSIONS="$pecl_extensions_arg" \
        --build-arg LOCAL_EXTENSIONS="$local_extensions_arg" \
        --build-arg DATABASE_EXTENSIONS="$database_extensions_arg" \
        --build-arg PHP_INI_CONFIGS="$php_ini_arg" \
        --build-arg ENV_VARS="$env_vars_arg" \
        --build-arg COMPOSER_MIRROR="$COMPOSER_MIRROR" \
        --build-arg BUILD_MODE="$BUILD_MODE" \
        --build-arg WORKDIR="$WORKDIR" \
        --tag "$final_image_name" \
        --progress=plain \
        . 2>&1 | tee -a "$BUILD_LOG_FILE"

    if [ ${PIPESTATUS[0]} -eq 0 ]; then
        log_success "最终镜像构建成功: $final_image_name"
        echo ""
        log_info "镜像详细信息:"
        docker images | grep -E "REPOSITORY|$final_image_name"
    else
        log_error "最终镜像构建失败,请查看日志: $BUILD_LOG_FILE"
        exit 1
    fi
}

# 生成构建报告
generate_build_report() {
    local final_image_name="$1"
    local report_file="./logs/build_report_$(date '+%Y%m%d_%H%M%S').txt"
    
    log_step "生成构建报告"
    
    {
        echo "========================================================"
        echo "           PHP Docker 构建报告"
        echo "========================================================"
        echo ""
        echo "构建时间: $(date '+%Y-%m-%d %H:%M:%S')"
        echo "最终镜像: $final_image_name"
        echo "基础镜像: $BASE_IMAGE_NAME"
        echo "系统类型: $SYSTEM_TYPE"
        echo "构建模式: $BUILD_MODE"
        echo ""
        echo "========================================================"
        echo "           已安装扩展列表"
        echo "========================================================"
        docker run --rm "$final_image_name" php -m
        echo ""
        echo "========================================================"
        echo "           PHP 版本信息"
        echo "========================================================"
        docker run --rm "$final_image_name" php -v
        echo ""
        echo "========================================================"
        echo "           Composer 版本"
        echo "========================================================"
        docker run --rm "$final_image_name" composer --version
        echo ""
        echo "========================================================"
        echo "           镜像大小"
        echo "========================================================"
        docker images | grep "$final_image_name"
        echo ""
    } > "$report_file"
    
    log_success "构建报告已生成: $report_file"
    cat "$report_file"
}

# 清理旧镜像
cleanup_old_images() {
    log_step "清理未使用的 Docker 镜像"
    echo -e "${YELLOW}是否清理未使用的镜像?(y/N)${NC}"
    read -r response
    if [[ "$response" =~ ^[Yy]$ ]]; then
        docker image prune -f
        log_success "镜像清理完成"
    else
        log_info "跳过镜像清理"
    fi
}

# 生成 docker-compose.yml
generate_docker_compose() {
    local image_name="$1"
    local compose_file="./docker-compose.yml"
    
    log_step "生成 docker-compose.yml"
    
    cat > "$compose_file" <<EOF
version: '3.8'

services:
  php:
    image: ${image_name}
    container_name: php-app
    restart: unless-stopped
    working_dir: ${WORKDIR}
    volumes:
      - ./app:${WORKDIR}
    networks:
      - app-network
    environment:
      - TZ=Asia/Shanghai
    healthcheck:
      test: ["CMD", "php", "-v"]
      interval: 30s
      timeout: 10s
      retries: 3

networks:
  app-network:
    driver: bridge
EOF

    log_success "docker-compose.yml 已生成: $compose_file"
}

# 主函数
main() {
    show_banner
    
    # 初始化变量
    BASE_IMAGE=""
    PECL_EXTENSIONS=()
    LOCAL_EXTENSIONS=()
    DATABASE_EXTENSIONS=()
    TOOLS=()
    BUILD_MODE="dev"
    CUSTOM_SOURCE="false"
    COMPOSER_MIRROR="auto"
    PHP_INI_CONFIGS=()
    BUILD_TARGET="all"
    FORCE_REBUILD_BASE="false"
    WORKDIR="/var/www/html"
    HEALTHCHECK=""
    ENV_VARS=()
    SYSTEM_TYPE=""
    BASE_IMAGE_NAME=""

    # 检查配置文件
    check_config_file

    # 解析配置
    parse_config

    # 检测系统类型
    detect_system_type

    # 生成基础镜像名称
    generate_base_image_name

    # 显示配置摘要
    show_config_summary

    # 根据构建目标执行
    case "$BUILD_TARGET" in
        base)
            log_info "构建目标: 仅构建基础镜像"
            build_base_image
            ;;
        final)
            log_info "构建目标: 仅构建最终镜像"
            if ! check_base_image_exists; then
                log_error "基础镜像不存在,请先构建基础镜像或设置 build_target=all"
                exit 1
            fi
            build_final_image "$1"
            generate_build_report "$1"
            generate_docker_compose "$1"
            ;;
        all)
            log_info "构建目标: 构建基础镜像和最终镜像"
            # 检查是否需要构建基础镜像
            if [ "$FORCE_REBUILD_BASE" = "true" ]; then
                log_warn "强制重新构建基础镜像"
                build_base_image
            else
                if check_base_image_exists; then
                    log_info "基础镜像已存在,跳过构建"
                else
                    build_base_image
                fi
            fi
            build_final_image "$1"
            generate_build_report "$1"
            generate_docker_compose "$1"
            ;;
        *)
            log_error "无效的构建目标: $BUILD_TARGET"
            exit 1
            ;;
    esac

    # 询问是否清理旧镜像
    cleanup_old_images

    echo -e "${GREEN}"
    echo "========================================================"
    echo "    ✅ 构建流程全部完成!"
    echo "========================================================"
    echo -e "${NC}"
    echo "📋 构建日志: $BUILD_LOG_FILE"
    echo "🐳 Docker 镜像: $1"
    echo "📝 使用方式: docker run -it --rm $1 php -v"
    echo "🚀 Docker Compose: docker-compose up -d"
    echo ""
}

# 执行主函数
main "$@"
  • 该文件是解析配置文件及控制构建情况的文件

执行构建

# 进入项目目录
cd project

# 添加文件执行权限
chmod +x ./build.sh

# 执行构建
./build.sh 新的镜像名称

结语

该份套餐可以做到:

  • 构建逻辑通用:不同版本、不同扩展的构建只需要修改 build.conf 配置文件
  • 扩展自由度高:可自由增减扩展信息,可指定版本,也可不指定,不指定时安装最新版本
  • 构建模式切换:分为 prod/dev,prod 构建会清楚构建缓存等信息,减少镜像体积;dev模式不会,方便多次构建,减少构建时间
  • 自定义系统依赖源:自由配置是否使用自定义系统依赖源,减少构建成功率和构建速度
  • 自定义php配置:不同的开发需求,可能需要不通过的php配置,可以自由配置,构建后覆盖原有的默认配置
  • 构建目标自由:可自由配置构建 基础镜像(base)、最终镜像(final)、基础+最终(all)
  • 系统依赖自动检测:可根据配置的基础镜像,自动识别镜像系统(debian/alpine)、并安装对应的依赖,如:系统依赖、系统工具等
  • GD 扩展自动配置:如果 local 扩展中存在 GD 扩展,则自动做扩展特殊配置
  • 镜像名称自定义:基础镜像名称自动生成,最终镜像名称可以自由定义
  • 扩展安装重试机制:安装扩展容易因为各种原因失败,追加重试机制(3次)减少失败次数

okk,提升到此结束,又可以愉快的做牛马了 ... ... ...