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

写在前面

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

背景

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

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

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

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

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

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

一、准备工作

环境准备

基于本人开发环境做说明

  • 一个 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

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 做兼容的
  • 该文件没有设置启动命令:是为了给多个项目自己的启动命令留空间

构建新镜像

shell 复制代码
# 执行构建
docker build -t 镜像名称:标签 .

# 检查新镜像
docker images

容器编排

docker-compose.yml

yaml 复制代码
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 编排容器是这样的:

yaml 复制代码
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 相关的参数配置

执行容器编排

shell 复制代码
# 后台运行,创建容器并启动
docker-compose up -d

三、启动并运行项目

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

shell 复制代码
docker exec -it hyperf01 bash

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

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

四、提升

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

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

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

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

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

目录结构

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

shell 复制代码
project
|--config
   |--build.conf
|--pecl
|--source
   |--alpine_source.list
   |--debian_source.list
|--build.sh
|--Dockerfile.base
|--Dockerfile.final
  • project:项目根目录
  • project/config:构建配置文件目录
  • project/config/build.conf:构建配置文件,方便修改构建信息
  • 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
shell 复制代码
# PHP 基础镜像
[base_image]
php:8.2-cli
#php:8.2-alpine
#php:7.4-cli
#php:7.4-alpine

# PECL 扩展(pecl install)
[pecl]
#swoole-4.8.13
#redis-5.3.7
swoole-5.1.3
redis

# 本地扩展(docker-php-ext-install)
[local]
pcntl
gd
zip
pdo_mysql
opcache

# 构建模式:prod / dev
[mode]
dev

# 是否使用自定义依赖源地址 true / false
[custom_source]
false

# composer 源:auto / aliyun / tencent / ustc / packagist
[composer_mirror]
auto

# php.ini 配置
[php]
swoole.use_shortname=Off
memory_limit=1G
opcache.enable_cli=on

# 构建目标:base / final / all
[build_target]
all

# 是否强制重新构建基础镜像:true / false
[force_rebuild_base]
false
alpine_source.list
shell 复制代码
# 阿里云源
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
shell 复制代码
# 阿里云源
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
dockerfile 复制代码
# =============================================================================
# PHP 基础镜像 Dockerfile
# 功能:安装系统依赖、编译工具和常用库,为后续扩展安装做准备
# =============================================================================

# 构建参数
ARG BASE_IMAGE
FROM ${BASE_IMAGE}

# 系统类型参数
ARG SYSTEM_TYPE
ARG CUSTOM_SOURCE

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

# 设置环境变量防止 OOM
ENV MAKEFLAGS="-j2"
ENV COMPOSER_MEMORY_LIMIT=-1

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

# 安装基础依赖和编译工具
RUN set -eux; \
    echo "========================================"; \
    echo "开始构建 PHP 基础镜像"; \
    echo "系统类型: ${SYSTEM_TYPE}"; \
    echo "自定义源: ${CUSTOM_SOURCE}"; \
    echo "========================================"; \
    \
    # Alpine 系统配置
    if [ "$SYSTEM_TYPE" = "alpine" ]; then \
        echo "配置 Alpine 源..."; \
        if [ "$CUSTOM_SOURCE" = "true" ]; then \
            echo "使用自定义源"; \
            cat /tmp/source.list > /etc/apk/repositories; \
        fi; \
        \
        echo "更新软件包索引..."; \
        apk update || { echo "APK 更新失败"; exit 1; }; \
        \
        echo "安装基础工具..."; \
        apk add --no-cache \
            bash \
            curl \
            wget \
            git \
            vim \
            unzip || { echo "基础工具安装失败"; exit 1; }; \
        \
        echo "安装开发库..."; \
        apk add --no-cache \
            libzip-dev \
            zlib-dev \
            libpng-dev \
            libjpeg-turbo-dev \
            freetype-dev \
            libwebp-dev || { echo "开发库安装失败"; exit 1; }; \
        \
        echo "安装 XPM 和加密库..."; \
        apk add --no-cache \
            libxpm-dev \
            openssl-dev \
            curl-dev || { echo "XPM/加密库安装失败"; exit 1; }; \
        \
        echo "安装 XML 和数据库库..."; \
        apk add --no-cache \
            libxml2-dev \
            oniguruma-dev \
            sqlite-dev \
            postgresql-dev || { echo "XML/数据库库安装失败"; exit 1; }; \
        \
        echo "安装编译工具..."; \
        apk add --no-cache \
            autoconf \
            g++ \
            make \
            linux-headers || { echo "编译工具安装失败"; exit 1; }; \
        \
        echo "配置 GD 扩展依赖..."; \
        docker-php-ext-configure gd \
            --with-freetype \
            --with-jpeg \
            --with-webp || { echo "GD 配置失败,继续..."; }; \
    \
    # Debian 系统配置
    else \
        echo "配置 Debian 源..."; \
        if [ "$CUSTOM_SOURCE" = "true" ]; then \
            echo "使用自定义源"; \
            cat /tmp/source.list > /etc/apt/sources.list; \
        fi; \
        \
        echo "更新软件包索引..."; \
        apt-get update || { echo "APT 更新失败"; exit 1; }; \
        \
        echo "安装基础工具和编译依赖..."; \
        DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
            curl \
            wget \
            git \
            vim \
            unzip \
            libzip-dev \
            zlib1g-dev \
            libpng-dev \
            libjpeg-dev \
            libfreetype6-dev \
            libwebp-dev \
            libxpm-dev \
            libssl-dev \
            libcurl4-openssl-dev \
            libxml2-dev \
            libonig-dev \
            libsqlite3-dev \
            libpq-dev \
            autoconf \
            g++ \
            make || { echo "Debian 包安装失败"; exit 1; }; \
        \
        echo "配置 GD 扩展依赖..."; \
        docker-php-ext-configure gd \
            --with-freetype \
            --with-jpeg \
            --with-webp || { echo "GD 配置失败,继续..."; }; \
        \
        echo "清理 APT 缓存..."; \
        rm -rf /var/lib/apt/lists/*; \
    fi; \
    \
    # 清理临时文件
    rm -rf /tmp/*; \
    \
    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
dockerfile 复制代码
# =============================================================================
# PHP 最终镜像 Dockerfile(多阶段构建)
# 功能:基于基础镜像安装 PHP 扩展、配置 PHP 和 Composer
# =============================================================================

# 构建参数
ARG BASE_IMAGE_NAME
FROM ${BASE_IMAGE_NAME}

# 扩展和配置参数
ARG SYSTEM_TYPE
ARG PECL_EXTENSIONS=""
ARG LOCAL_EXTENSIONS=""
ARG PHP_INI_CONFIGS=""
ARG COMPOSER_MIRROR="auto"
ARG BUILD_MODE="dev"

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

# 复制本地扩展目录(如果存在)
COPY pecl/ /tmp/pecl/

# 安装 PHP 扩展
RUN set -eux; \
    echo "========================================"; \
    echo "开始安装 PHP 扩展"; \
    echo "系统类型: ${SYSTEM_TYPE}"; \
    echo "PECL 扩展: ${PECL_EXTENSIONS}"; \
    echo "LOCAL 扩展: ${LOCAL_EXTENSIONS}"; \
    echo "构建模式: ${BUILD_MODE}"; \
    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; \
    }; \
    \
    # 安装 LOCAL 扩展
    if [ -n "$LOCAL_EXTENSIONS" ]; then \
        echo "----------------------------------------"; \
        echo "安装 LOCAL 扩展: $LOCAL_EXTENSIONS"; \
        echo "----------------------------------------"; \
        \
        # 特殊扩展依赖处理
        for ext in $LOCAL_EXTENSIONS; do \
            case "$ext" in \
                gd) \
                    echo "检测到 GD 扩展,已在基础镜像配置"; \
                    ;; \
                zip) \
                    echo "检测到 ZIP 扩展"; \
                    if [ "$SYSTEM_TYPE" = "alpine" ]; then \
                        apk add --no-cache libzip-dev; \
                    fi; \
                    ;; \
                pdo_mysql) \
                    echo "检测到 PDO_MYSQL 扩展"; \
                    ;; \
                opcache) \
                    echo "检测到 OPCACHE 扩展"; \
                    ;; \
                pcntl) \
                    echo "检测到 PCNTL 扩展"; \
                    ;; \
                *) \
                    echo "扩展: $ext"; \
                    ;; \
            esac; \
        done; \
        \
        # 一次性安装所有 LOCAL 扩展
        if ! retry_command "docker-php-ext-install -j\$(nproc) $LOCAL_EXTENSIONS"; then \
            echo "LOCAL 扩展安装失败"; \
            exit 1; \
        fi; \
        echo "LOCAL 扩展安装成功"; \
    fi; \
    \
    # 安装 PECL 扩展
    if [ -n "$PECL_EXTENSIONS" ]; then \
        echo "----------------------------------------"; \
        echo "安装 PECL 扩展: $PECL_EXTENSIONS"; \
        echo "----------------------------------------"; \
        \
        echo "$PECL_EXTENSIONS" | tr ',' '\n' | while read -r ext; do \
            # 跳过空行
            [ -z "$ext" ] && continue; \
            \
            ext_name=$(echo "$ext" | cut -d'-' -f1); \
            echo "处理扩展: $ext_name (完整名称: $ext)"; \
            \
            # 检查本地扩展目录
            if [ -f "/tmp/pecl/${ext}.tgz" ]; then \
                echo "从本地安装: ${ext}"; \
                if ! retry_command "pecl install /tmp/pecl/${ext}.tgz"; then \
                    echo "从本地安装失败,尝试从 PECL 仓库安装"; \
                    if ! retry_command "pecl install $ext"; then \
                        echo "扩展 $ext 安装失败"; \
                        exit 1; \
                    fi; \
                fi; \
            else \
                echo "从 PECL 仓库安装: ${ext}"; \
                if ! retry_command "pecl install $ext"; then \
                    echo "扩展 $ext 安装失败"; \
                    exit 1; \
                fi; \
            fi; \
            \
            # 启用扩展
            echo "启用扩展: $ext_name"; \
            docker-php-ext-enable "$ext_name"; \
        done; \
        echo "PECL 扩展安装成功"; \
    fi; \
    \
    # 验证扩展安装
    echo "----------------------------------------"; \
    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 "PHP.ini 配置完成"; \
        cat /usr/local/etc/php/conf.d/custom.ini; \
    fi;

# 安装 Composer
RUN set -eux; \
    echo "========================================"; \
    echo "安装 Composer"; \
    echo "Composer 镜像: ${COMPOSER_MIRROR}"; \
    echo "========================================"; \
    \
    # 下载 Composer
    curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \
    \
    # 配置 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
    composer --version; \
    echo "Composer 安装完成";

# 清理和优化
RUN set -eux; \
    echo "========================================"; \
    echo "清理临时文件和优化镜像"; \
    echo "构建模式: ${BUILD_MODE}"; \
    echo "========================================"; \
    \
    # 清理 PECL 缓存
    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; \
        else \
            apt-get purge -y autoconf g++ make; \
            apt-get autoremove -y; \
            rm -rf /var/lib/apt/lists/*; \
        fi; \
    else \
        echo "开发模式:保留编译工具"; \
    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 "========================================";

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

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

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

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

# 配置文件路径
CONFIG_FILE="./config/build.conf"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

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

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

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

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

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

# 解析配置文件
parse_config() {
    local section=""
    local key=""
    
    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")
                    ;;
                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"
                    ;;
            esac
        fi
    done < "$CONFIG_FILE"
}

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

# 生成基础镜像名称
generate_base_image_name() {
    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() {
    if docker image inspect "$BASE_IMAGE_NAME" >/dev/null 2>&1; then
        log_info "基础镜像已存在: $BASE_IMAGE_NAME"
        return 0
    else
        log_info "基础镜像不存在,需要构建"
        return 1
    fi
}

# 构建基础镜像
build_base_image() {
    log_info "开始构建基础镜像..."
    
    # 构建参数
    docker build \
        --file Dockerfile.base \
        --build-arg BASE_IMAGE="$BASE_IMAGE" \
        --build-arg SYSTEM_TYPE="$SYSTEM_TYPE" \
        --build-arg CUSTOM_SOURCE="$CUSTOM_SOURCE" \
        --tag "$BASE_IMAGE_NAME" \
        --progress=plain \
        .
    
    if [ $? -eq 0 ]; then
        log_success "基础镜像构建成功: $BASE_IMAGE_NAME"
    else
        log_error "基础镜像构建失败"
        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_info "开始构建最终镜像: $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
    
    # 准备 PHP 配置参数
    local php_ini_arg=""
    if [ ${#PHP_INI_CONFIGS[@]} -gt 0 ]; then
        php_ini_arg=$(IFS='|'; echo "${PHP_INI_CONFIGS[*]}")
    fi
    
    # 构建最终镜像
    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 PHP_INI_CONFIGS="$php_ini_arg" \
        --build-arg COMPOSER_MIRROR="$COMPOSER_MIRROR" \
        --build-arg BUILD_MODE="$BUILD_MODE" \
        --tag "$final_image_name" \
        --progress=plain \
        .
    
    if [ $? -eq 0 ]; then
        log_success "最终镜像构建成功: $final_image_name"
        log_info "镜像信息:"
        docker images | grep "$final_image_name"
    else
        log_error "最终镜像构建失败"
        exit 1
    fi
}

# 主函数
main() {
    log_info "=========================================="
    log_info "PHP Docker 多阶段构建开始"
    log_info "=========================================="
    
    # 初始化变量
    BASE_IMAGE=""
    PECL_EXTENSIONS=()
    LOCAL_EXTENSIONS=()
    BUILD_MODE="dev"
    CUSTOM_SOURCE="false"
    COMPOSER_MIRROR="auto"
    PHP_INI_CONFIGS=()
    BUILD_TARGET="all"
    FORCE_REBUILD_BASE="false"
    SYSTEM_TYPE=""
    BASE_IMAGE_NAME=""
    
    # 检查配置文件
    check_config_file
    
    # 解析配置
    log_info "开始解析配置文件..."
    parse_config
    
    # 检测系统类型
    detect_system_type
    
    # 生成基础镜像名称
    generate_base_image_name
    
    # 显示配置信息
    log_info "=========================================="
    log_info "配置信息:"
    log_info "  基础镜像: $BASE_IMAGE"
    log_info "  系统类型: $SYSTEM_TYPE"
    log_info "  基础镜像名称: $BASE_IMAGE_NAME"
    log_info "  PECL 扩展: ${PECL_EXTENSIONS[*]}"
    log_info "  LOCAL 扩展: ${LOCAL_EXTENSIONS[*]}"
    log_info "  构建模式: $BUILD_MODE"
    log_info "  自定义源: $CUSTOM_SOURCE"
    log_info "  Composer 镜像: $COMPOSER_MIRROR"
    log_info "  构建目标: $BUILD_TARGET"
    log_info "  强制重建基础镜像: $FORCE_REBUILD_BASE"
    log_info "=========================================="
    
    # 根据构建目标执行
    case "$BUILD_TARGET" in
        base)
            log_info "构建目标: 仅构建基础镜像"
            build_base_image
            ;;
        final)
            log_info "构建目标: 仅构建最终镜像"
            if ! check_base_image_exists; then
                log_error "基础镜像不存在,请先构建基础镜像"
                exit 1
            fi
            build_final_image "$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"
            ;;
        *)
            log_error "无效的构建目标: $BUILD_TARGET"
            exit 1
            ;;
    esac
    
    log_success "=========================================="
    log_success "构建流程完成!"
    log_success "=========================================="
}

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

执行构建

shell 复制代码
# 进入项目目录
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,提升到此结束,又可以愉快的做牛马了 ... ... ...

相关推荐
q***48411 小时前
SpringBoot实战(三十二)集成 ofdrw,实现 PDF 和 OFD 的转换、SM2 签署OFD
spring boot·后端·pdf
Oriental1 小时前
URL解码踩坑记录
前端·后端
IT教程资源C1 小时前
(N_135)基于springboot,vue高校图书馆管理系统
vue.js·spring boot·后端
程序员西西2 小时前
Spring Boot 整合 Zookeeper实现分布式锁?
后端
库森学长2 小时前
多线程有序执行,九大方案!
后端·面试
z***67772 小时前
Spring EL 表达式的简单介绍和使用
java·后端·spring
Dajiaonew2 小时前
SpringCloud Stream 快速入门
后端·spring·spring cloud
用户69371750013842 小时前
8.Kotlin 类:类的基础:主构造函数与次构造函数
android·后端·kotlin
用户69371750013842 小时前
9.Kotlin 类:类的核心:属性 (Property) 与自定义访问器 (Getter/Setter)
android·后端·kotlin