写在前面
《搭建Hyperf本地开发环境之Docker容器开发(二)》是继 《搭建Hyperf本地开发环境之Docker容器开发(一)》的补充说明;
为什么要做这个补充呢,是因为我发现我在安装 oracle 扩展的时候,发现不能成功,所以重新做了改动来满足我的需求;
初衷还是不变,都是通过修改配置文件来控制一切;
快闪回放
目录结构
project
|--config
|--build.conf
|--extension_deps.conf
|--extension_special.conf
|--system_tools.conf
|--extension
|--logs
|--build.sh
|--Dockerfile
文件内容及说明
build.conf
# ========================================
# PHP Hyperf Docker 构建配置文件
# ========================================
# PHP 基础镜像(只能选择一个,其他注释掉)
[base_image]
#php:8.3-cli
#php:8.3-cli-alpine
#php:8.2-cli
#php:8.2-cli-alpine
#php:8.1-cli
#php:8.1-cli-alpine
php:7.4-cli
#php:7.4-cli-alpine
# ========================================
# 扩展配置
# 格式说明:
# 扩展名 - 安装最新版本
# 扩展名-版本号 - 安装指定版本
# local:扩展名 - 强制从本地安装
# pecl:扩展名 - 强制从PECL安装
# ========================================
[extension]
# 核心扩展(docker-php-ext-install)
pcntl
pdo_mysql
bcmath
sockets
# GD库(自动处理依赖)
gd
# 压缩扩展
zip
# PECL扩展
redis
swoole-4.8.13
# 可选扩展(按需取消注释)
#mongodb
#xlswriter
#protobuf
#grpc
#imagick
#memcached
#oci8
pdo_oci
# SQL Server php7.4 对应 pdo_sqlsrv-5.10.1
pdo_sqlsrv-5.10.1
#sqlsrv
pdo_pgsql
#pgsql
#amqp
#rdkafka
#igbinary
#msgpack
#yaf
#yar
#apcu
#xdebug
# ========================================
# 构建模式:prod / dev
# prod: 生产模式,优化构建,清理缓存
# dev: 开发模式,保留调试信息
# ========================================
[mode]
prod
# ========================================
# 项目工作目录
# ========================================
[workdir]
/opt/www
# ========================================
# APT/APK 系统镜像源(用于加速系统包下载)
# 可选:default / aliyun / ustc / tsinghua / 163
# default: 使用官方源
# ========================================
[system_mirror]
aliyun
# ========================================
# Composer 镜像源
# 可选:auto / aliyun / tencent / ustc / packagist / huaweicloud
# auto: 自动检测最快的源
# ========================================
[composer_mirror]
auto
# ========================================
# PHP 配置(php.ini)
# 格式:配置项=值
# ========================================
[php]
swoole.use_shortname=Off
memory_limit=1G
opcache.enable_cli=On
opcache.jit_buffer_size=128M
date.timezone=Asia/Shanghai
max_execution_time=0
post_max_size=100M
upload_max_filesize=100M
# ========================================
# Docker CMD 命令配置
# 留空则不设置CMD
# ========================================
[cmd]
#php /opt/www/bin/hyperf.php start
# ========================================
# 常用系统工具
# 支持的工具列表见 config/system_tools.conf
# 脚本会自动识别系统类型安装对应的包
# ========================================
[system_tools]
# 网络诊断工具
#curl
wget
telnet
#netcat
#ping
#traceroute
#dig
#nslookup
ss
netstat
lsof
#tcpdump
#iptables
# 编辑器和文本处理
vim
#nano
#less
#jq
#tree
# 系统监控工具
#htop
#top
#ps
#iostat
#vmstat
#strace
# 文件和压缩工具
#tar
#gzip
#unzip
#zip
#file
# 其他实用工具
#git
#procps
#coreutils
#findutils
#grep
#sed
#awk
# ========================================
# 额外的系统包(空格分隔,直接指定包名)
# ========================================
[extra_packages]
# ========================================
# 环境变量配置
# 格式:变量名=值
# ========================================
[env]
#APP_ENV=dev
#SCAN_CACHEABLE=false
# ========================================
# 构建参数
# ========================================
[build_args]
# 启用构建缓存
enable_cache=true
# 并行编译数(0为自动检测)
parallel_jobs=0
# 内存限制(防止OOM,单位MB,0为不限制)
memory_limit=2048
# 清理构建缓存
clean_cache=true
extension_deps.conf
# ========================================
# PHP 扩展依赖配置文件
# 格式:扩展名|Debian依赖|Alpine依赖
# 多个依赖用逗号分隔
# ========================================
# 核心扩展
pcntl||
pdo_mysql||
bcmath||
sockets||
calendar||
exif||
gettext||
shmop||
sysvmsg||
sysvsem||
sysvshm||
# GD库
gd|libpng-dev,libjpeg-dev,libfreetype6-dev,libwebp-dev,libxpm-dev|libpng-dev,libjpeg-turbo-dev,freetype-dev,libwebp-dev,libxpm-dev
# 压缩相关
zip|libzip-dev|libzip-dev
bz2|libbz2-dev|bzip2-dev
zlib||zlib-dev
# 图像处理
imagick|libmagickwand-dev|imagemagick-dev
# 数据库扩展
pdo_pgsql|libpq-dev|postgresql-dev
pgsql|libpq-dev|postgresql-dev
pdo_sqlite|libsqlite3-dev|sqlite-dev
sqlite3|libsqlite3-dev|sqlite-dev
# Redis/Memcached
redis||
memcached|libmemcached-dev,zlib1g-dev|libmemcached-dev,zlib-dev
# 消息队列
amqp|librabbitmq-dev|rabbitmq-c-dev
rdkafka|librdkafka-dev|librdkafka-dev
# 其他常用扩展
swoole|libcurl4-openssl-dev,libssl-dev,libc-ares-dev|curl-dev,openssl-dev,c-ares-dev
mongodb|libssl-dev|openssl-dev
xlswriter|zlib1g-dev|zlib-dev
protobuf||
grpc||
igbinary||
msgpack||
apcu||
xdebug||
yaf||
yar|libcurl4-openssl-dev|curl-dev
# 国际化
intl|libicu-dev|icu-dev
iconv||
# 加密
sodium|libsodium-dev|libsodium-dev
mcrypt|libmcrypt-dev|libmcrypt-dev
openssl||
# XML相关
xml|libxml2-dev|libxml2-dev
dom|libxml2-dev|libxml2-dev
xmlrpc|libxml2-dev|libxml2-dev
xmlreader|libxml2-dev|libxml2-dev
xmlwriter|libxml2-dev|libxml2-dev
xsl|libxslt-dev|libxslt-dev
simplexml|libxml2-dev|libxml2-dev
soap|libxml2-dev|libxml2-dev
# LDAP
ldap|libldap2-dev|openldap-dev
# 其他
curl|libcurl4-openssl-dev|curl-dev
gmp|libgmp-dev|gmp-dev
imap|libc-client-dev,libkrb5-dev|imap-dev,krb5-dev
snmp|libsnmp-dev|net-snmp-dev
tidy|libtidy-dev|tidyhtml-dev
ftp||
fileinfo||
phar||
posix||
readline|libreadline-dev|readline-dev
tokenizer||
ctype||
filter||
json||
mbstring|libonig-dev|oniguruma-dev
pdo||
reflection||
session||
standard||
opcache||
extension_special.conf
# ========================================
# PHP 扩展特殊配置文件
# 需要特殊处理的扩展配置
# ========================================
# ----------------------------------------
# GD 库配置
# ----------------------------------------
[gd]
# Debian 配置命令
debian_configure=docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp --with-xpm
# Alpine 配置命令
alpine_configure=docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp --with-xpm
# PHP 7.x 兼容配置(不同的参数格式)
php7_debian_configure=docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ --with-webp-dir=/usr/include/ --with-xpm-dir=/usr/include/
php7_alpine_configure=docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ --with-webp-dir=/usr/include/ --with-xpm-dir=/usr/include/
# ----------------------------------------
# Oracle 扩展配置 (oci8, pdo_oci)
# ----------------------------------------
[oci8]
# 依赖:需要 Oracle Instant Client
# 注意:oci8 需要单独安装,不能和其他核心扩展一起批量安装
install_method=separate
pre_install=<<EOF
# 下载并安装 Oracle Instant Client
log_info "安装 Oracle Instant Client..."
mkdir -p /opt/oracle
cd /opt/oracle
# 设置 Oracle 版本
ORACLE_VERSION="21_14"
ORACLE_MAJOR="21"
# Debian
if command -v apt-get &>/dev/null; then
apt-get update && apt-get install -y libaio1 libaio-dev unzip wget
# 下载 Oracle Instant Client
if [ ! -f "instantclient-basic-linux.x64-${ORACLE_MAJOR}.*.zip" ]; then
wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-basic-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-basic.zip || \
wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-basic-linuxx64.zip" -O instantclient-basic.zip
fi
if [ ! -f "instantclient-sdk-linux.x64-${ORACLE_MAJOR}.*.zip" ]; then
wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-sdk-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-sdk.zip || \
wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-sdk-linuxx64.zip" -O instantclient-sdk.zip
fi
unzip -o instantclient-basic.zip
unzip -o instantclient-sdk.zip
rm -f *.zip
# 获取实际安装目录
INSTANT_CLIENT_DIR=$(ls -d /opt/oracle/instantclient_* 2>/dev/null | head -1)
if [ -z "$INSTANT_CLIENT_DIR" ]; then
log_error "Oracle Instant Client 安装失败"
fi
# 配置链接库
echo "$INSTANT_CLIENT_DIR" > /etc/ld.so.conf.d/oracle-instantclient.conf
ldconfig
fi
# Alpine
if command -v apk &>/dev/null; then
apk add --no-cache libaio libnsl libc6-compat unzip wget
if [ ! -f "instantclient-basic.zip" ]; then
wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-basic-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-basic.zip || \
wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-basic-linuxx64.zip" -O instantclient-basic.zip
fi
if [ ! -f "instantclient-sdk.zip" ]; then
wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-sdk-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-sdk.zip || \
wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-sdk-linuxx64.zip" -O instantclient-sdk.zip
fi
unzip -o instantclient-basic.zip
unzip -o instantclient-sdk.zip
rm -f *.zip
INSTANT_CLIENT_DIR=$(ls -d /opt/oracle/instantclient_* 2>/dev/null | head -1)
if [ -z "$INSTANT_CLIENT_DIR" ]; then
log_error "Oracle Instant Client 安装失败"
fi
# Alpine 兼容性
ln -sf /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2 2>/dev/null || true
fi
# 导出环境变量(写入 profile 以便后续脚本使用)
export ORACLE_HOME="$INSTANT_CLIENT_DIR"
export LD_LIBRARY_PATH="$INSTANT_CLIENT_DIR:$LD_LIBRARY_PATH"
echo "export ORACLE_HOME=$INSTANT_CLIENT_DIR" >> /etc/profile
echo "export LD_LIBRARY_PATH=$INSTANT_CLIENT_DIR:\$LD_LIBRARY_PATH" >> /etc/profile
log_info "Oracle Instant Client 安装完成: $INSTANT_CLIENT_DIR"
# 直接安装 oci8 扩展
log_step "安装 oci8 扩展..."
docker-php-ext-configure oci8 --with-oci8=instantclient,$INSTANT_CLIENT_DIR
docker-php-ext-install -j$(nproc) oci8
log_info "oci8 扩展安装成功"
EOF
# 标记为已在 pre_install 中完成安装
skip_install=true
[pdo_oci]
# pdo_oci 需要单独安装
install_method=separate
pre_install=<<EOF
# 检查 Oracle Instant Client 是否已安装(如果没有安装 oci8 的话)
INSTANT_CLIENT_DIR=$(ls -d /opt/oracle/instantclient_* 2>/dev/null | head -1)
if [ -z "$INSTANT_CLIENT_DIR" ]; then
log_info "安装 Oracle Instant Client for pdo_oci..."
mkdir -p /opt/oracle
cd /opt/oracle
ORACLE_VERSION="21_14"
ORACLE_MAJOR="21"
if command -v apt-get &>/dev/null; then
apt-get update && apt-get install -y libaio1 libaio-dev unzip wget
wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-basic-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-basic.zip || \
wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-basic-linuxx64.zip" -O instantclient-basic.zip
wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-sdk-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-sdk.zip || \
wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-sdk-linuxx64.zip" -O instantclient-sdk.zip
unzip -o instantclient-basic.zip
unzip -o instantclient-sdk.zip
rm -f *.zip
INSTANT_CLIENT_DIR=$(ls -d /opt/oracle/instantclient_* 2>/dev/null | head -1)
echo "$INSTANT_CLIENT_DIR" > /etc/ld.so.conf.d/oracle-instantclient.conf
ldconfig
fi
if command -v apk &>/dev/null; then
apk add --no-cache libaio libnsl libc6-compat unzip wget
wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-basic-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-basic.zip || \
wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-basic-linuxx64.zip" -O instantclient-basic.zip
wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-sdk-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-sdk.zip || \
wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-sdk-linuxx64.zip" -O instantclient-sdk.zip
unzip -o instantclient-basic.zip
unzip -o instantclient-sdk.zip
rm -f *.zip
INSTANT_CLIENT_DIR=$(ls -d /opt/oracle/instantclient_* 2>/dev/null | head -1)
ln -sf /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2 2>/dev/null || true
fi
echo "export ORACLE_HOME=$INSTANT_CLIENT_DIR" >> /etc/profile
echo "export LD_LIBRARY_PATH=$INSTANT_CLIENT_DIR:\$LD_LIBRARY_PATH" >> /etc/profile
fi
export ORACLE_HOME="$INSTANT_CLIENT_DIR"
export LD_LIBRARY_PATH="$INSTANT_CLIENT_DIR:$LD_LIBRARY_PATH"
log_info "Oracle Instant Client 路径: $INSTANT_CLIENT_DIR"
# 直接安装 pdo_oci 扩展
log_step "安装 pdo_oci 扩展..."
docker-php-ext-configure pdo_oci --with-pdo-oci=instantclient,$INSTANT_CLIENT_DIR
docker-php-ext-install -j$(nproc) pdo_oci
log_info "pdo_oci 扩展安装成功"
EOF
# 标记为已在 pre_install 中完成安装
skip_install=true
# ----------------------------------------
# SQL Server 扩展配置 (sqlsrv, pdo_sqlsrv)
# ----------------------------------------
[sqlsrv]
pre_install=<<EOF
# 安装 Microsoft ODBC Driver
if command -v apt-get &>/dev/null; then
# 安装 gnupg 和 curl
apt-get update
apt-get install -y gnupg curl
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg
#curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
curl -fsSL https://packages.microsoft.com/config/debian/$(cat /etc/debian_version | cut -d. -f1)/prod.list > /etc/apt/sources.list.d/mssql-release.list
apt-get update
ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
fi
if command -v apk &>/dev/null; then
apk add --no-cache unixodbc-dev
# Alpine 需要从 edge 仓库安装 msodbcsql
echo "Note: SQL Server driver on Alpine requires manual setup"
fi
EOF
# PECL 安装
install_method=pecl
[pdo_sqlsrv]
pre_install=<<EOF
# 复用 sqlsrv 的 ODBC 驱动安装
if ! command -v sqlcmd &>/dev/null; then
if command -v apt-get &>/dev/null; then
# 安装 gnupg 和 curl
apt-get update
apt-get install -y gnupg curl
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg
#curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
curl -fsSL https://packages.microsoft.com/config/debian/$(cat /etc/debian_version | cut -d. -f1)/prod.list > /etc/apt/sources.list.d/mssql-release.list
apt-get update
ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
fi
if command -v apk &>/dev/null; then
apk add --no-cache unixodbc-dev
fi
fi
EOF
install_method=pecl
# ----------------------------------------
# PostgreSQL 扩展配置
# ----------------------------------------
[pdo_pgsql]
# Debian 额外配置
debian_extra=ln -sf /usr/include/postgresql /usr/include/pgsql
[pgsql]
debian_extra=ln -sf /usr/include/postgresql /usr/include/pgsql
# ----------------------------------------
# IMAP 扩展配置
# ----------------------------------------
[imap]
debian_configure=docker-php-ext-configure imap --with-kerberos --with-imap-ssl
alpine_configure=docker-php-ext-configure imap --with-kerberos --with-imap-ssl
# ----------------------------------------
# LDAP 扩展配置
# ----------------------------------------
[ldap]
debian_extra=ln -sf /usr/lib/x86_64-linux-gnu/libldap.so /usr/lib/libldap.so && ln -sf /usr/lib/x86_64-linux-gnu/liblber.so /usr/lib/liblber.so
debian_configure=docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu
# ----------------------------------------
# Swoole 扩展配置
# ----------------------------------------
[swoole]
# Swoole 编译参数
pecl_configure=--enable-openssl --enable-sockets --enable-mysqlnd --enable-swoole-curl --enable-cares
# PHP 7.x 兼容配置
php7_pecl_configure=--enable-openssl --enable-sockets --enable-mysqlnd --enable-http2
# ----------------------------------------
# GrPC 扩展配置
# ----------------------------------------
[grpc]
# grpc 需要大量内存编译
build_memory=4096
# 编译超时时间(秒)
build_timeout=3600
# ----------------------------------------
# xlswriter 扩展配置
# ----------------------------------------
[xlswriter]
pecl_configure=--enable-reader
# ----------------------------------------
# imagick 扩展配置
# ----------------------------------------
[imagick]
# 需要 ImageMagick 开发库
post_install=<<EOF
# 确保 ImageMagick 策略允许 PDF 处理
if [ -f /etc/ImageMagick-6/policy.xml ]; then
sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml
fi
if [ -f /etc/ImageMagick-7/policy.xml ]; then
sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-7/policy.xml
fi
EOF
# ----------------------------------------
# memcached 扩展配置
# ----------------------------------------
[memcached]
pecl_configure=--enable-memcached-igbinary --enable-memcached-json --enable-memcached-session --enable-memcached-sasl
# ----------------------------------------
# xdebug 扩展配置
# ----------------------------------------
[xdebug]
# 仅在 dev 模式安装
mode_only=dev
# xdebug 3.x 配置
php_ini_config=<<EOF
xdebug.mode=develop,debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
EOF
# ----------------------------------------
# opcache 扩展配置
# ----------------------------------------
[opcache]
php_ini_config=<<EOF
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=100000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.fast_shutdown=1
EOF
# 生产模式特殊配置
prod_php_ini_config=<<EOF
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=100000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.fast_shutdown=1
opcache.jit_buffer_size=256M
opcache.jit=1255
EOF
system_tools.conf
# ========================================
# 系统工具映射配置
# 格式: 工具名|debian包名|alpine包名|说明
# 如果包名为 - 表示该系统不支持或不需要安装
# ========================================
# 网络诊断工具
curl|curl|curl|命令行URL工具
wget|wget|wget|文件下载工具
telnet|telnet|busybox-extras|远程登录协议客户端
netcat|netcat-openbsd|netcat-openbsd|网络连接工具
nc|netcat-openbsd|netcat-openbsd|netcat别名
ping|iputils-ping|iputils|网络连通性测试
traceroute|traceroute|traceroute|路由追踪
tracepath|iputils-tracepath|iputils|路径MTU发现
dig|dnsutils|bind-tools|DNS查询工具
nslookup|dnsutils|bind-tools|DNS查询工具
host|dnsutils|bind-tools|DNS查询工具
ss|iproute2|iproute2|套接字统计工具
netstat|net-tools|net-tools|网络统计工具(旧版)
ip|iproute2|iproute2|IP配置工具
ifconfig|net-tools|net-tools|网络接口配置(旧版)
lsof|lsof|lsof|列出打开的文件
tcpdump|tcpdump|tcpdump|网络抓包工具
iptables|iptables|iptables|防火墙规则管理
nmap|nmap|nmap|网络扫描工具
mtr|mtr|mtr|网络诊断工具
# 编辑器和文本处理
vim|vim|vim|Vi改进版编辑器
vi|vim|vim|vim别名
nano|nano|nano|简易文本编辑器
less|less|less|分页查看器
more|util-linux|util-linux|分页查看器
cat|coreutils|coreutils|文件查看
head|coreutils|coreutils|查看文件头部
tail|coreutils|coreutils|查看文件尾部
jq|jq|jq|JSON处理工具
yq|yq|yq|YAML处理工具
tree|tree|tree|目录树显示
diff|diffutils|diffutils|文件比较
# 系统监控工具
htop|htop|htop|交互式进程查看器
top|procps|procps|进程监控
ps|procps|procps|进程状态
free|procps|procps|内存使用情况
uptime|procps|procps|系统运行时间
watch|procps|procps|周期性执行命令
pstree|psmisc|psmisc|进程树显示
killall|psmisc|psmisc|按名称终止进程
fuser|psmisc|psmisc|显示文件使用者
iostat|sysstat|sysstat|IO统计
vmstat|procps|procps|虚拟内存统计
sar|sysstat|sysstat|系统活动报告
mpstat|sysstat|sysstat|CPU统计
pidstat|sysstat|sysstat|进程统计
strace|strace|strace|系统调用追踪
ltrace|ltrace|-|库调用追踪
dstat|dstat|-|资源统计工具
iotop|iotop|iotop|IO监控
iftop|iftop|iftop|网络带宽监控
nload|nload|nload|网络流量监控
# 文件和压缩工具
tar|tar|tar|归档工具
gzip|gzip|gzip|gzip压缩
gunzip|gzip|gzip|gzip解压
bzip2|bzip2|bzip2|bzip2压缩
bunzip2|bzip2|bzip2|bzip2解压
xz|xz-utils|xz|xz压缩
unxz|xz-utils|xz|xz解压
zip|zip|zip|zip压缩
unzip|unzip|unzip|zip解压
7z|p7zip-full|p7zip|7zip压缩
rar|unrar|-|RAR解压
file|file|file|文件类型识别
stat|coreutils|coreutils|文件状态
du|coreutils|coreutils|磁盘使用统计
df|coreutils|coreutils|磁盘空间统计
ln|coreutils|coreutils|链接创建
cp|coreutils|coreutils|文件复制
mv|coreutils|coreutils|文件移动
rm|coreutils|coreutils|文件删除
mkdir|coreutils|coreutils|创建目录
rmdir|coreutils|coreutils|删除目录
chmod|coreutils|coreutils|权限修改
chown|coreutils|coreutils|所有者修改
touch|coreutils|coreutils|修改时间戳
# 查找和搜索工具
find|findutils|findutils|文件查找
locate|mlocate|mlocate|快速文件定位
which|debianutils|which|命令路径查找
whereis|util-linux|util-linux|命令位置查找
grep|grep|grep|文本搜索
egrep|grep|grep|扩展正则搜索
fgrep|grep|grep|固定字符串搜索
ack|ack|ack|代码搜索工具
ag|silversearcher-ag|the_silver_searcher|快速代码搜索
rg|ripgrep|-|更快的代码搜索
# 文本处理
sed|sed|sed|流编辑器
awk|gawk|gawk|文本处理语言
gawk|gawk|gawk|GNU AWK
cut|coreutils|coreutils|文本切割
sort|coreutils|coreutils|排序
uniq|coreutils|coreutils|去重
wc|coreutils|coreutils|字数统计
tr|coreutils|coreutils|字符转换
tee|coreutils|coreutils|输出分流
xargs|findutils|findutils|参数传递
column|bsdmainutils|util-linux|列格式化
# 版本控制
git|git|git|Git版本控制
svn|subversion|subversion|SVN版本控制
# 系统管理
sudo|sudo|sudo|权限提升
su|login|shadow|用户切换
useradd|passwd|shadow|添加用户
usermod|passwd|shadow|修改用户
userdel|passwd|shadow|删除用户
groupadd|passwd|shadow|添加组
passwd|passwd|passwd|修改密码
crontab|cron|busybox|定时任务
systemctl|systemd|-|服务管理(Debian)
service|sysvinit-utils|openrc|服务管理
rc-service|-|openrc|服务管理(Alpine)
# 开发工具
make|make|make|构建工具
gcc|gcc|gcc|C编译器
g++|g++|g++|C++编译器
python|python3|python3|Python解释器
python3|python3|python3|Python3解释器
pip|python3-pip|py3-pip|Python包管理
node|nodejs|nodejs|Node.js
npm|npm|npm|Node包管理
perl|perl|perl|Perl解释器
# 数据库客户端
mysql|mariadb-client|mariadb-client|MySQL客户端
psql|postgresql-client|postgresql-client|PostgreSQL客户端
redis-cli|redis-tools|redis|Redis客户端
mongo|mongodb-clients|-|MongoDB客户端
sqlite3|sqlite3|sqlite|SQLite客户端
# 其他实用工具
procps|procps|procps|进程工具集
coreutils|coreutils|coreutils|核心工具集
util-linux|util-linux|util-linux|系统工具集
ca-certificates|ca-certificates|ca-certificates|CA证书
openssl|openssl|openssl|SSL工具
tzdata|tzdata|tzdata|时区数据
locales|locales|-|语言环境(仅Debian)
bash|bash|bash|Bash shell
zsh|zsh|zsh|Zsh shell
tmux|tmux|tmux|终端复用器
screen|screen|screen|终端复用器
rsync|rsync|rsync|文件同步
sshpass|sshpass|sshpass|SSH密码工具
expect|expect|expect|自动化交互工具
bc|bc|bc|计算器
xxd|xxd|vim|十六进制查看
hexdump|bsdmainutils|util-linux|十六进制转储
base64|coreutils|coreutils|Base64编解码
md5sum|coreutils|coreutils|MD5校验
sha256sum|coreutils|coreutils|SHA256校验
build.sh
#!/bin/bash
# ========================================
# PHP Hyperf Docker 构建脚本
# 用法: ./build.sh 镜像名称:标签
# ========================================
set -e
set -o pipefail # 确保管道中任何一步失败,整个命令都返回错误
# ========================================
# 全局变量
# ========================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_DIR="${SCRIPT_DIR}/config"
BUILD_SCRIPTS_DIR="${SCRIPT_DIR}/build_scripts"
EXTENSION_DIR="${SCRIPT_DIR}/extension"
LOGS_DIR="${SCRIPT_DIR}/logs"
LOG_FILE="${LOGS_DIR}/build_$(date +%Y%m%d_%H%M%S).log"
# 配置文件
BUILD_CONF="${CONFIG_DIR}/build.conf"
EXTENSION_DEPS_CONF="${CONFIG_DIR}/extension_deps.conf"
EXTENSION_SPECIAL_CONF="${CONFIG_DIR}/extension_special.conf"
# 默认值
DEFAULT_WORKDIR="/opt/www"
DEFAULT_MODE="prod"
DEFAULT_COMPOSER_MIRROR="auto"
# Bash 版本检查
BASH_VERSION_MAJOR="${BASH_VERSION%%.*}"
# ========================================
# 颜色定义
# ========================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
NC='\033[0m' # No Color
BOLD='\033[1m'
BLINK='\033[5m'
# ========================================
# 日志函数
# ========================================
log_info() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $1"
echo -e "${GREEN}${msg}${NC}"
echo "$msg" >> "$LOG_FILE"
}
log_warn() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $1"
echo -e "${YELLOW}${msg}${NC}"
echo "$msg" >> "$LOG_FILE"
}
log_error() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $1"
echo -e "${RED}${BOLD}${msg}${NC}"
echo "$msg" >> "$LOG_FILE"
}
log_success() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [SUCCESS] $1"
echo -e "${GREEN}${BOLD}${msg}${NC}"
echo "$msg" >> "$LOG_FILE"
}
log_step() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [STEP] $1"
echo -e "${CYAN}${BOLD}══════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN}${BOLD} $1${NC}"
echo -e "${CYAN}${BOLD}══════════════════════════════════════════════════════════${NC}"
echo "$msg" >> "$LOG_FILE"
}
log_debug() {
if [ "$DEBUG" = "true" ]; then
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [DEBUG] $1"
echo -e "${MAGENTA}${msg}${NC}"
echo "$msg" >> "$LOG_FILE"
fi
}
# ========================================
# 进度显示函数
# ========================================
SPINNER_PID=""
start_spinner() {
local msg="$1"
local delay=0.1
local spinstr='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
(
while true; do
for (( i=0; i<${#spinstr}; i++ )); do
echo -ne "\r${CYAN}${BLINK}${spinstr:$i:1}${NC} ${msg} "
sleep $delay
done
done
) &
SPINNER_PID=$!
disown
}
stop_spinner() {
local status="$1"
if [ -n "$SPINNER_PID" ]; then
kill $SPINNER_PID 2>/dev/null || true
wait $SPINNER_PID 2>/dev/null || true
SPINNER_PID=""
fi
if [ "$status" = "success" ]; then
echo -e "\r${GREEN}✔${NC} $2 "
elif [ "$status" = "fail" ]; then
echo -e "\r${RED}✘${NC} $2 "
else
echo -e "\r $2 "
fi
}
# 进度条
show_progress() {
local current=$1
local total=$2
local msg="$3"
local width=50
local percent=$((current * 100 / total))
local filled=$((current * width / total))
local empty=$((width - filled))
printf "\r${CYAN}[${NC}"
printf "%${filled}s" | tr ' ' '█'
printf "%${empty}s" | tr ' ' '░'
printf "${CYAN}]${NC} ${percent}%% ${msg}"
if [ "$current" -eq "$total" ]; then
echo ""
fi
}
# ========================================
# 配置解析函数
# ========================================
declare -A CONFIG
declare -a EXTENSIONS
declare -a SYSTEM_TOOLS
declare -A PHP_CONFIG
declare -A ENV_VARS
declare -A BUILD_ARGS
declare -A TOOLS_DEBIAN
declare -A TOOLS_ALPINE
parse_config() {
log_step "解析配置文件"
if [ ! -f "$BUILD_CONF" ]; then
log_error "配置文件不存在: $BUILD_CONF"
exit 1
fi
start_spinner "正在解析 build.conf..."
local current_section=""
local line_num=0
while IFS= read -r line || [ -n "$line" ]; do
line_num=$((line_num + 1))
# 去除首尾空白
line="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
# 跳过空行和注释
if [ -z "$line" ] || [[ "$line" =~ ^# ]]; then
continue
fi
# 检测段落
if [[ "$line" =~ ^\[([a-zA-Z_]+)\]$ ]]; then
current_section="${BASH_REMATCH[1]}"
log_debug "进入段落: $current_section"
continue
fi
# 根据段落解析内容
case "$current_section" in
base_image)
if [[ ! "$line" =~ ^# ]]; then
CONFIG["base_image"]="$line"
log_debug "基础镜像: $line"
fi
;;
extension)
EXTENSIONS+=("$line")
log_debug "扩展: $line"
;;
mode)
CONFIG["mode"]="$line"
;;
workdir)
CONFIG["workdir"]="$line"
;;
composer_mirror)
CONFIG["composer_mirror"]="$line"
;;
system_mirror)
CONFIG["system_mirror"]="$line"
;;
php)
if [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
local key="${BASH_REMATCH[1]}"
local value="${BASH_REMATCH[2]}"
PHP_CONFIG["$key"]="$value"
fi
;;
cmd)
CONFIG["cmd"]="$line"
;;
system_tools)
SYSTEM_TOOLS+=("$line")
log_debug "系统工具: $line"
;;
extra_packages)
if [ -n "$line" ]; then
CONFIG["extra_packages"]="${CONFIG["extra_packages"]} $line"
fi
;;
env)
if [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
ENV_VARS["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}"
fi
;;
build_args)
if [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
BUILD_ARGS["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}"
fi
;;
esac
done < "$BUILD_CONF"
# 设置默认值
CONFIG["base_image"]="${CONFIG["base_image"]:-php:8.2-cli}"
CONFIG["mode"]="${CONFIG["mode"]:-$DEFAULT_MODE}"
CONFIG["workdir"]="${CONFIG["workdir"]:-$DEFAULT_WORKDIR}"
CONFIG["composer_mirror"]="${CONFIG["composer_mirror"]:-$DEFAULT_COMPOSER_MIRROR}"
CONFIG["system_mirror"]="${CONFIG["system_mirror"]:-default}"
stop_spinner "success" "配置解析完成"
# 显示解析结果
log_info "基础镜像: ${CONFIG["base_image"]}"
log_info "构建模式: ${CONFIG["mode"]}"
log_info "工作目录: ${CONFIG["workdir"]}"
log_info "系统镜像源: ${CONFIG["system_mirror"]}"
log_info "Composer源: ${CONFIG["composer_mirror"]}"
log_info "扩展数量: ${#EXTENSIONS[@]}"
log_info "系统工具数量: ${#SYSTEM_TOOLS[@]}"
}
# ========================================
# 检测镜像类型
# ========================================
detect_image_type() {
local image="${CONFIG["base_image"]}"
if [[ "$image" =~ alpine ]]; then
CONFIG["os_type"]="alpine"
CONFIG["pkg_manager"]="apk"
else
CONFIG["os_type"]="debian"
CONFIG["pkg_manager"]="apt-get"
fi
# 提取 PHP 版本
if [[ "$image" =~ php:([0-9]+\.[0-9]+) ]]; then
CONFIG["php_version"]="${BASH_REMATCH[1]}"
CONFIG["php_major"]="${CONFIG["php_version"]%%.*}"
else
CONFIG["php_version"]="8.2"
CONFIG["php_major"]="8"
fi
log_info "操作系统类型: ${CONFIG["os_type"]}"
log_info "PHP 版本: ${CONFIG["php_version"]}"
}
# ========================================
# 扩展依赖解析
# ========================================
declare -A EXT_DEPS_DEBIAN
declare -A EXT_DEPS_ALPINE
parse_extension_deps() {
log_step "解析扩展依赖配置"
if [ ! -f "$EXTENSION_DEPS_CONF" ]; then
log_warn "扩展依赖配置文件不存在: $EXTENSION_DEPS_CONF"
return
fi
while IFS='|' read -r ext debian alpine; do
# 跳过注释和空行
[[ "$ext" =~ ^# ]] && continue
[ -z "$ext" ] && continue
ext="$(echo "$ext" | tr -d '[:space:]')"
EXT_DEPS_DEBIAN["$ext"]="$debian"
EXT_DEPS_ALPINE["$ext"]="$alpine"
done < "$EXTENSION_DEPS_CONF"
log_info "已加载 ${#EXT_DEPS_DEBIAN[@]} 个扩展依赖配置"
}
# ========================================
# 扩展特殊配置解析
# ========================================
declare -A EXT_SPECIAL
parse_extension_special() {
log_step "解析扩展特殊配置"
if [ ! -f "$EXTENSION_SPECIAL_CONF" ]; then
log_warn "扩展特殊配置文件不存在: $EXTENSION_SPECIAL_CONF"
return
fi
local current_ext=""
local current_key=""
local in_heredoc=false
local heredoc_content=""
while IFS= read -r line || [ -n "$line" ]; do
# 检测扩展段落
if [[ "$line" =~ ^\[([a-zA-Z0-9_]+)\]$ ]]; then
current_ext="${BASH_REMATCH[1]}"
continue
fi
# 跳过注释和空行
[[ "$line" =~ ^[[:space:]]*# ]] && continue
if [ -n "$current_ext" ]; then
# 处理 heredoc
if [ "$in_heredoc" = true ]; then
if [[ "$line" =~ ^EOF$ ]]; then
EXT_SPECIAL["${current_ext}_${current_key}"]="$heredoc_content"
in_heredoc=false
heredoc_content=""
else
heredoc_content="${heredoc_content}${line}\n"
fi
continue
fi
# 检测 heredoc 开始
if [[ "$line" =~ ^([a-zA-Z_]+)=\<\<EOF$ ]]; then
current_key="${BASH_REMATCH[1]}"
in_heredoc=true
heredoc_content=""
continue
fi
# 普通键值对
if [[ "$line" =~ ^([a-zA-Z_]+)=(.+)$ ]]; then
local key="${BASH_REMATCH[1]}"
local value="${BASH_REMATCH[2]}"
EXT_SPECIAL["${current_ext}_${key}"]="$value"
fi
fi
done < "$EXTENSION_SPECIAL_CONF"
log_info "已加载扩展特殊配置"
}
# ========================================
# 解析系统工具配置
# ========================================
resolve_system_tools() {
log_step "解析系统工具映射"
local tools_conf="${CONFIG_DIR}/system_tools.conf"
if [ ! -f "$tools_conf" ]; then
log_warn "系统工具映射文件不存在: $tools_conf"
return
fi
local os_type="${CONFIG["os_type"]}"
local tool_packages=""
# 读取配置文件建立映射
declare -A TOOLS_MAP
while IFS='|' read -r tool debian alpine desc; do
[[ "$tool" =~ ^# ]] && continue
[ -z "$tool" ] && continue
tool="$(echo "$tool" | tr -d '[:space:]')"
if [ "$os_type" = "alpine" ]; then
TOOLS_MAP["$tool"]="$(echo "$alpine" | tr -d '[:space:]')"
else
TOOLS_MAP["$tool"]="$(echo "$debian" | tr -d '[:space:]')"
fi
done < "$tools_conf"
# 匹配选中的工具
for tool in "${SYSTEM_TOOLS[@]}"; do
# 去除注释
[[ "$tool" =~ ^# ]] && continue
local pkg_name="${TOOLS_MAP[$tool]}"
if [ -n "$pkg_name" ] && [ "$pkg_name" != "-" ]; then
tool_packages="$tool_packages $pkg_name"
log_debug "映射工具: $tool -> $pkg_name"
else
log_warn "未找到工具 $tool 的对应包名或该系统不支持"
fi
done
# 将结果保存到全局配置中,供后续使用
CONFIG["resolved_tools"]="$tool_packages"
log_info "已解析系统工具包: $tool_packages"
}
# ========================================
# 检查本地扩展
# ========================================
declare -A LOCAL_EXTENSIONS
check_local_extensions() {
log_step "检查本地扩展目录"
if [ ! -d "$EXTENSION_DIR" ]; then
log_info "本地扩展目录不存在,跳过本地扩展检查"
return
fi
local ext_count=0
shopt -s nullglob
local ext_files=("$EXTENSION_DIR"/*.tgz "$EXTENSION_DIR"/*.tar.gz "$EXTENSION_DIR"/*.zip)
shopt -u nullglob
for ext_file in "${ext_files[@]}"; do
[ -f "$ext_file" ] || continue
local filename=$(basename "$ext_file")
local ext_name=""
local ext_version=""
# 解析扩展名和版本
if [[ "$filename" =~ ^([a-zA-Z_]+)-([0-9.]+)\.(tgz|tar\.gz|zip)$ ]]; then
ext_name="${BASH_REMATCH[1]}"
ext_version="${BASH_REMATCH[2]}"
elif [[ "$filename" =~ ^([a-zA-Z_]+)\.(tgz|tar\.gz|zip)$ ]]; then
ext_name="${BASH_REMATCH[1]}"
ext_version="latest"
fi
if [ -n "$ext_name" ]; then
# 检查是否已存在该扩展的记录
if [ -n "${LOCAL_EXTENSIONS[$ext_name]}" ]; then
# 比较版本,保留更高版本
local existing_version="${LOCAL_EXTENSIONS[$ext_name]%%|*}"
if version_compare "$ext_version" "$existing_version"; then
LOCAL_EXTENSIONS["$ext_name"]="${ext_version}|${ext_file}"
log_debug "更新本地扩展: $ext_name 版本 $existing_version -> $ext_version"
fi
else
LOCAL_EXTENSIONS["$ext_name"]="${ext_version}|${ext_file}"
log_debug "发现本地扩展: $ext_name 版本 $ext_version"
fi
ext_count=$((ext_count + 1))
fi
done
log_info "发现 ${#LOCAL_EXTENSIONS[@]} 个本地扩展"
}
# 版本比较函数
version_compare() {
local v1="$1"
local v2="$2"
[ "$v1" = "latest" ] && return 0
[ "$v2" = "latest" ] && return 1
if [ "$(printf '%s\n' "$v1" "$v2" | sort -V | tail -n1)" = "$v1" ]; then
return 0
else
return 1
fi
}
# ========================================
# 判断扩展类型
# ========================================
# 核心扩展列表(使用 docker-php-ext-install)
CORE_EXTENSIONS=(
"bcmath" "bz2" "calendar" "ctype" "curl" "dba" "dom" "enchant"
"exif" "fileinfo" "filter" "ftp" "gd" "gettext" "gmp" "hash"
"iconv" "imap" "intl" "json" "ldap" "mbstring" "mysqli" "oci8"
"odbc" "opcache" "pcntl" "pdo" "pdo_dblib" "pdo_firebird" "pdo_mysql"
"pdo_oci" "pdo_odbc" "pdo_pgsql" "pdo_sqlite" "pgsql" "phar" "posix"
"pspell" "readline" "reflection" "session" "shmop" "simplexml"
"snmp" "soap" "sockets" "sodium" "spl" "standard" "sysvmsg"
"sysvsem" "sysvshm" "tidy" "tokenizer" "xml" "xmlreader" "xmlrpc"
"xmlwriter" "xsl" "zip" "zlib"
)
is_core_extension() {
local ext="$1"
for core_ext in "${CORE_EXTENSIONS[@]}"; do
[ "$ext" = "$core_ext" ] && return 0
done
return 1
}
# ========================================
# 生成安装脚本
# ========================================
generate_install_scripts() {
log_step "生成安装脚本"
# 创建脚本目录
mkdir -p "$BUILD_SCRIPTS_DIR"
local os_type="${CONFIG["os_type"]}"
local php_major="${CONFIG["php_major"]}"
local total_ext=${#EXTENSIONS[@]}
local current_ext=0
# 解析系统工具
resolve_system_tools
# 收集所有依赖
local all_deps=""
local install_core=""
local install_pecl=""
local install_local=""
local configure_cmds=""
local pre_install_cmds=""
local post_install_cmds=""
for ext_entry in "${EXTENSIONS[@]}"; do
current_ext=$((current_ext + 1))
show_progress $current_ext $total_ext "处理扩展: $ext_entry"
# 解析扩展名和版本
local ext_name=""
local ext_version=""
local force_local=false
local force_pecl=false
# 检查强制标记
if [[ "$ext_entry" =~ ^local:(.+)$ ]]; then
ext_entry="${BASH_REMATCH[1]}"
force_local=true
elif [[ "$ext_entry" =~ ^pecl:(.+)$ ]]; then
ext_entry="${BASH_REMATCH[1]}"
force_pecl=true
fi
# 解析版本号
if [[ "$ext_entry" =~ ^([a-zA-Z_]+)-([0-9.]+)$ ]]; then
ext_name="${BASH_REMATCH[1]}"
ext_version="${BASH_REMATCH[2]}"
else
ext_name="$ext_entry"
ext_version=""
fi
# 检查是否仅限特定模式
local mode_only="${EXT_SPECIAL["${ext_name}_mode_only"]}"
if [ -n "$mode_only" ] && [ "$mode_only" != "${CONFIG["mode"]}" ]; then
log_debug "跳过扩展 $ext_name (仅限 $mode_only 模式)"
continue
fi
# 获取依赖
local deps=""
if [ "$os_type" = "alpine" ]; then
deps="${EXT_DEPS_ALPINE[$ext_name]}"
else
deps="${EXT_DEPS_DEBIAN[$ext_name]}"
fi
if [ -n "$deps" ]; then
all_deps="$all_deps $deps"
fi
# 检查预安装命令
local pre_install="${EXT_SPECIAL["${ext_name}_pre_install"]}"
if [ -n "$pre_install" ]; then
pre_install_cmds="${pre_install_cmds}\n# Pre-install for $ext_name\n$(echo -e "$pre_install")"
fi
# 检查额外配置
local extra_cmd=""
if [ "$os_type" = "debian" ]; then
extra_cmd="${EXT_SPECIAL["${ext_name}_debian_extra"]}"
fi
if [ -n "$extra_cmd" ]; then
pre_install_cmds="${pre_install_cmds}\n$extra_cmd"
fi
# 确定安装方式
local install_method="${EXT_SPECIAL["${ext_name}_install_method"]}"
# 检查是否跳过安装(已在 pre_install 中完成)
local skip_install="${EXT_SPECIAL["${ext_name}_skip_install"]}"
if [ "$skip_install" = "true" ]; then
log_debug "扩展 $ext_name 将在 pre_install 脚本中安装"
continue
fi
# 检查本地扩展
if [ "$force_local" = true ] || [ -n "${LOCAL_EXTENSIONS[$ext_name]}" ]; then
local local_info="${LOCAL_EXTENSIONS[$ext_name]}"
if [ -n "$local_info" ]; then
local local_file="${local_info#*|}"
install_local="${install_local}\n# Install $ext_name from local\ninstall_local_extension \"$ext_name\" \"$local_file\""
continue
fi
fi
# 检查是否为核心扩展
if is_core_extension "$ext_name" && [ "$force_pecl" != true ] && [ "$install_method" != "pecl" ]; then
# 检查配置命令
local configure_cmd=""
if [ "$php_major" -lt 8 ]; then
configure_cmd="${EXT_SPECIAL["${ext_name}_php7_${os_type}_configure"]}"
fi
if [ -z "$configure_cmd" ]; then
configure_cmd="${EXT_SPECIAL["${ext_name}_${os_type}_configure"]}"
fi
if [ -n "$configure_cmd" ]; then
configure_cmds="${configure_cmds}\n$configure_cmd"
fi
install_core="${install_core} $ext_name"
else
# PECL 安装
local pecl_name="$ext_name"
if [ -n "$ext_version" ]; then
pecl_name="${ext_name}-${ext_version}"
fi
# 检查 PECL 配置参数
local pecl_configure=""
if [ "$php_major" -lt 8 ]; then
pecl_configure="${EXT_SPECIAL["${ext_name}_php7_pecl_configure"]}"
fi
if [ -z "$pecl_configure" ]; then
pecl_configure="${EXT_SPECIAL["${ext_name}_pecl_configure"]}"
fi
if [ -n "$pecl_configure" ]; then
install_pecl="${install_pecl}\ninstall_pecl_extension \"$pecl_name\" \"$pecl_configure\""
else
install_pecl="${install_pecl}\ninstall_pecl_extension \"$pecl_name\" \"\""
fi
fi
# 检查后置安装命令
local post_install="${EXT_SPECIAL["${ext_name}_post_install"]}"
if [ -n "$post_install" ]; then
post_install_cmds="${post_install_cmds}\n# Post-install for $ext_name\n$(echo -e "$post_install")"
fi
# 检查 PHP 配置
local php_ini_config=""
if [ "${CONFIG["mode"]}" = "prod" ]; then
php_ini_config="${EXT_SPECIAL["${ext_name}_prod_php_ini_config"]}"
fi
if [ -z "$php_ini_config" ]; then
php_ini_config="${EXT_SPECIAL["${ext_name}_php_ini_config"]}"
fi
if [ -n "$php_ini_config" ]; then
# 添加到 PHP 配置
while IFS= read -r ini_line; do
[ -z "$ini_line" ] && continue
if [[ "$ini_line" =~ ^([^=]+)=(.*)$ ]]; then
PHP_CONFIG["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}"
fi
done <<< "$(echo -e "$php_ini_config")"
fi
done
# 去重依赖
all_deps=$(echo "$all_deps" | tr ',' ' ' | tr ' ' '\n' | sort -u | tr '\n' ' ')
# log_info "生成依赖安装脚本..."
# generate_deps_script "$all_deps"
# 合并扩展依赖和系统工具包
local final_deps="${all_deps} ${CONFIG["resolved_tools"]}"
# 去重处理
final_deps=$(echo "$final_deps" | tr ' ' '\n' | sort -u | tr '\n' ' ')
log_info "生成依赖安装脚本(包含系统工具)..."
generate_deps_script "$final_deps"
log_info "生成扩展安装脚本..."
generate_extensions_script "$install_core" "$install_pecl" "$install_local" "$configure_cmds" "$pre_install_cmds"
log_info "生成后置安装脚本..."
generate_post_install_script "$post_install_cmds"
log_info "生成 Composer 安装脚本..."
generate_composer_script
log_info "生成 PHP 配置文件..."
generate_php_ini
log_info "生成环境变量文件..."
generate_env_file
# log_info "生成入口点脚本..."
# generate_entrypoint_script
log_success "所有安装脚本生成完成"
}
# ========================================
# 生成依赖安装脚本
# ========================================
generate_deps_script() {
local deps="$1"
local os_type="${CONFIG["os_type"]}"
local system_mirror="${CONFIG["system_mirror"]}"
cat > "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_HEADER'
#!/bin/bash
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
log_info "开始安装系统依赖..."
SCRIPT_HEADER
if [ "$os_type" = "alpine" ]; then
# 生成 Alpine 镜像源切换脚本
if [ "$system_mirror" != "default" ]; then
cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << SCRIPT_ALPINE_MIRROR
# 切换 Alpine 镜像源
log_info "切换 Alpine 镜像源到: $system_mirror"
SCRIPT_ALPINE_MIRROR
case "$system_mirror" in
aliyun)
cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_ALPINE_ALIYUN'
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
SCRIPT_ALPINE_ALIYUN
;;
ustc)
cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_ALPINE_USTC'
sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
SCRIPT_ALPINE_USTC
;;
tsinghua)
cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_ALPINE_TSINGHUA'
sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
SCRIPT_ALPINE_TSINGHUA
;;
163)
cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_ALPINE_163'
sed -i 's/dl-cdn.alpinelinux.org/mirrors.163.com/g' /etc/apk/repositories
SCRIPT_ALPINE_163
;;
esac
fi
cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << SCRIPT_ALPINE
# Alpine 系统依赖
apk update
apk add --no-cache --virtual .build-deps \\
\$PHPIZE_DEPS \\
autoconf \\
automake \\
libtool \\
make \\
gcc \\
g++ \\
linux-headers
# 安装运行时依赖
apk add --no-cache \\
$deps
log_info "Alpine 系统依赖安装完成"
SCRIPT_ALPINE
else
# 生成 Debian 镜像源切换脚本
if [ "$system_mirror" != "default" ]; then
cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << SCRIPT_DEBIAN_MIRROR
# 切换 Debian 镜像源
log_info "切换 Debian/Ubuntu 镜像源到: $system_mirror"
# 备份原始源
cp /etc/apt/sources.list /etc/apt/sources.list.bak 2>/dev/null || true
# 检测 Debian 版本
DEBIAN_VERSION=\$(cat /etc/os-release | grep VERSION_CODENAME | cut -d= -f2)
if [ -z "\$DEBIAN_VERSION" ]; then
DEBIAN_VERSION="bullseye"
fi
log_info "检测到 Debian 版本: \$DEBIAN_VERSION"
SCRIPT_DEBIAN_MIRROR
case "$system_mirror" in
aliyun)
cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_DEBIAN_ALIYUN'
cat > /etc/apt/sources.list << EOF
deb https://mirrors.aliyun.com/debian/ $DEBIAN_VERSION main contrib non-free
deb https://mirrors.aliyun.com/debian/ $DEBIAN_VERSION-updates main contrib non-free
deb https://mirrors.aliyun.com/debian-security $DEBIAN_VERSION-security main contrib non-free
EOF
SCRIPT_DEBIAN_ALIYUN
;;
ustc)
cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_DEBIAN_USTC'
cat > /etc/apt/sources.list << EOF
deb https://mirrors.ustc.edu.cn/debian/ $DEBIAN_VERSION main contrib non-free
deb https://mirrors.ustc.edu.cn/debian/ $DEBIAN_VERSION-updates main contrib non-free
deb https://mirrors.ustc.edu.cn/debian-security $DEBIAN_VERSION-security main contrib non-free
EOF
SCRIPT_DEBIAN_USTC
;;
tsinghua)
cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_DEBIAN_TSINGHUA'
cat > /etc/apt/sources.list << EOF
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ $DEBIAN_VERSION main contrib non-free
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ $DEBIAN_VERSION-updates main contrib non-free
deb https://mirrors.tuna.tsinghua.edu.cn/debian-security $DEBIAN_VERSION-security main contrib non-free
EOF
SCRIPT_DEBIAN_TSINGHUA
;;
163)
cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_DEBIAN_163'
cat > /etc/apt/sources.list << EOF
deb http://mirrors.163.com/debian/ $DEBIAN_VERSION main contrib non-free
deb http://mirrors.163.com/debian/ $DEBIAN_VERSION-updates main contrib non-free
deb http://mirrors.163.com/debian-security $DEBIAN_VERSION-security main contrib non-free
EOF
SCRIPT_DEBIAN_163
;;
esac
fi
cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << SCRIPT_DEBIAN
# Debian 系统依赖
apt-get update
apt-get install -y --no-install-recommends \\
\$PHPIZE_DEPS \\
autoconf \\
automake \\
libtool \\
make \\
gcc \\
g++ \\
$deps
log_info "Debian 系统依赖安装完成"
SCRIPT_DEBIAN
fi
chmod +x "$BUILD_SCRIPTS_DIR/install_deps.sh"
}
# ========================================
# 生成扩展安装脚本
# ========================================
generate_extensions_script() {
local core_exts="$1"
local pecl_exts="$2"
local local_exts="$3"
local configure_cmds="$4"
local pre_install_cmds="$5"
cat > "$BUILD_SCRIPTS_DIR/install_extensions.sh" << 'SCRIPT_HEADER'
#!/bin/bash
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
log_step() { echo -e "${CYAN}[STEP]${NC} $1"; }
# PECL 扩展安装函数
install_pecl_extension() {
local ext_name="$1"
local configure_opts="$2"
log_step "安装 PECL 扩展: $ext_name"
if [ -n "$configure_opts" ]; then
printf "\n" | pecl install $ext_name <<< "$configure_opts" || {
log_warn "带配置安装失败,尝试默认安装..."
printf "\n" | pecl install $ext_name
}
else
printf "\n" | pecl install $ext_name || log_error "安装 $ext_name 失败"
fi
# 获取纯扩展名(去除版本号)
local pure_name="${ext_name%%-*}"
docker-php-ext-enable $pure_name || log_error "启用 $pure_name 失败"
log_info "PECL 扩展 $ext_name 安装成功"
}
# 本地扩展安装函数
install_local_extension() {
local ext_name="$1"
local ext_file="$2"
log_step "从本地安装扩展: $ext_name"
local ext_dir="/tmp/ext_$ext_name"
mkdir -p "$ext_dir"
case "$ext_file" in
*.tgz|*.tar.gz)
tar -xzf "$ext_file" -C "$ext_dir" --strip-components=1
;;
*.zip)
unzip -q "$ext_file" -d "$ext_dir"
;;
*)
log_error "不支持的扩展格式: $ext_file"
;;
esac
cd "$ext_dir"
phpize
./configure
make -j$(nproc)
make install
docker-php-ext-enable $ext_name
rm -rf "$ext_dir"
log_info "本地扩展 $ext_name 安装成功"
}
log_info "开始安装 PHP 扩展..."
SCRIPT_HEADER
# 添加预安装命令
if [ -n "$pre_install_cmds" ]; then
echo -e "\n# 预安装命令" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
echo -e "$pre_install_cmds" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
fi
# 添加配置命令
if [ -n "$configure_cmds" ]; then
echo -e "\n# 扩展配置命令" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
echo -e "$configure_cmds" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
fi
# 添加核心扩展安装
if [ -n "$core_exts" ]; then
cat >> "$BUILD_SCRIPTS_DIR/install_extensions.sh" << SCRIPT_CORE
# 安装核心扩展
log_step "安装核心扩展:$core_exts"
docker-php-ext-install -j\$(nproc)$core_exts || log_error "核心扩展安装失败"
log_info "核心扩展安装成功"
SCRIPT_CORE
fi
# 添加 PECL 扩展安装
if [ -n "$pecl_exts" ]; then
echo -e "\n# 安装 PECL 扩展" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
echo -e "$pecl_exts" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
fi
# 添加本地扩展安装
if [ -n "$local_exts" ]; then
echo -e "\n# 安装本地扩展" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
echo -e "$local_exts" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
fi
echo -e '\nlog_info "所有 PHP 扩展安装完成"' >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
chmod +x "$BUILD_SCRIPTS_DIR/install_extensions.sh"
}
# ========================================
# 生成后置安装脚本
# ========================================
generate_post_install_script() {
local post_cmds="$1"
cat > "$BUILD_SCRIPTS_DIR/post_install.sh" << 'SCRIPT_HEADER'
#!/bin/bash
set -e
log_info() { echo -e "\033[0;32m[INFO]\033[0m $1"; }
log_info "执行后置安装脚本..."
SCRIPT_HEADER
if [ -n "$post_cmds" ]; then
echo -e "$post_cmds" >> "$BUILD_SCRIPTS_DIR/post_install.sh"
fi
echo -e '\nlog_info "后置安装脚本执行完成"' >> "$BUILD_SCRIPTS_DIR/post_install.sh"
chmod +x "$BUILD_SCRIPTS_DIR/post_install.sh"
}
# ========================================
# 生成 Composer 安装脚本
# ========================================
generate_composer_script() {
local mirror="${CONFIG["composer_mirror"]}"
cat > "$BUILD_SCRIPTS_DIR/install_composer.sh" << 'SCRIPT_HEADER'
#!/bin/bash
set -e
log_info() { echo -e "\033[0;32m[INFO]\033[0m $1"; }
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $1"; exit 1; }
MIRROR="$1"
log_info "安装 Composer..."
# 下载并安装 Composer
EXPECTED_CHECKSUM="$(php -r 'copy("https://composer.github.io/installer.sig", "php://stdout");')"
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"
if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
rm composer-setup.php
log_error 'Composer installer 校验失败'
fi
php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php
# 配置镜像源
configure_mirror() {
case "$MIRROR" in
aliyun)
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
;;
tencent)
composer config -g repo.packagist composer https://mirrors.cloud.tencent.com/composer/
;;
ustc)
composer config -g repo.packagist composer https://mirrors.ustc.edu.cn/composer/
;;
huaweicloud)
composer config -g repo.packagist composer https://repo.huaweicloud.com/repository/php/
;;
packagist)
composer config -g repo.packagist composer https://packagist.org
;;
auto)
# 自动检测最快的源
log_info "自动检测最快的 Composer 源..."
MIRRORS=(
"https://mirrors.aliyun.com/composer/"
"https://mirrors.cloud.tencent.com/composer/"
"https://packagist.org"
)
FASTEST=""
FASTEST_TIME=999
for m in "${MIRRORS[@]}"; do
TIME=$(curl -o /dev/null -s -w '%{time_total}' --connect-timeout 3 "$m" 2>/dev/null || echo "999")
if (( $(echo "$TIME < $FASTEST_TIME" | bc -l 2>/dev/null || echo 0) )); then
FASTEST_TIME=$TIME
FASTEST=$m
fi
done
if [ -n "$FASTEST" ]; then
composer config -g repo.packagist composer "$FASTEST"
log_info "使用最快源: $FASTEST"
fi
;;
esac
}
configure_mirror
log_info "Composer 安装完成"
composer --version
SCRIPT_HEADER
chmod +x "$BUILD_SCRIPTS_DIR/install_composer.sh"
}
# ========================================
# 生成 PHP 配置文件
# ========================================
generate_php_ini() {
local ini_file="$BUILD_SCRIPTS_DIR/php.ini"
echo "; PHP Custom Configuration" > "$ini_file"
echo "; Generated by build.sh at $(date)" >> "$ini_file"
echo "" >> "$ini_file"
for key in "${!PHP_CONFIG[@]}"; do
echo "$key=${PHP_CONFIG[$key]}" >> "$ini_file"
done
log_debug "PHP 配置文件生成完成"
}
# ========================================
# 生成环境变量文件
# ========================================
generate_env_file() {
local env_file="$BUILD_SCRIPTS_DIR/env.sh"
echo "# Environment Variables" > "$env_file"
echo "# Generated by build.sh at $(date)" >> "$env_file"
echo "" >> "$env_file"
for key in "${!ENV_VARS[@]}"; do
echo "export $key=\"${ENV_VARS[$key]}\"" >> "$env_file"
done
}
# ========================================
# 生成入口点脚本
# ========================================
generate_entrypoint_script() {
local cmd="${CONFIG["cmd"]}"
cat > "$BUILD_SCRIPTS_DIR/entrypoint.sh" << 'SCRIPT_HEADER'
#!/bin/bash
set -e
# 加载环境变量
if [ -f /etc/profile ]; then
source /etc/profile
fi
# 如果传入了命令,执行传入的命令
if [ $# -gt 0 ]; then
exec "$@"
fi
SCRIPT_HEADER
# 添加默认命令
if [ -n "$cmd" ]; then
echo "# 默认命令" >> "$BUILD_SCRIPTS_DIR/entrypoint.sh"
echo "exec $cmd" >> "$BUILD_SCRIPTS_DIR/entrypoint.sh"
else
echo "# 无默认命令,保持容器运行" >> "$BUILD_SCRIPTS_DIR/entrypoint.sh"
echo 'exec tail -f /dev/null' >> "$BUILD_SCRIPTS_DIR/entrypoint.sh"
fi
chmod +x "$BUILD_SCRIPTS_DIR/entrypoint.sh"
}
# ========================================
# 验证配置
# ========================================
validate_config() {
log_step "验证配置"
local errors=0
# 验证基础镜像
if [ -z "${CONFIG["base_image"]}" ]; then
log_error "未配置基础镜像"
errors=$((errors + 1))
fi
# 验证扩展数量
if [ ${#EXTENSIONS[@]} -eq 0 ]; then
log_warn "未配置任何扩展"
fi
# 验证工作目录
if [ -z "${CONFIG["workdir"]}" ]; then
log_warn "未配置工作目录,使用默认值: $DEFAULT_WORKDIR"
CONFIG["workdir"]="$DEFAULT_WORKDIR"
fi
if [ $errors -gt 0 ]; then
log_error "配置验证失败,发现 $errors 个错误"
exit 1
fi
log_success "配置验证通过"
}
# ========================================
# Docker 构建
# ========================================
build_docker_image() {
local image_tag="$1"
log_step "开始构建 Docker 镜像: $image_tag"
# 准备构建参数
local build_args=""
build_args="$build_args --build-arg BASE_IMAGE=${CONFIG["base_image"]}"
build_args="$build_args --build-arg WORKDIR=${CONFIG["workdir"]}"
build_args="$build_args --build-arg BUILD_MODE=${CONFIG["mode"]}"
build_args="$build_args --build-arg COMPOSER_MIRROR=${CONFIG["composer_mirror"]}"
# 传递额外包参数
if [ -n "${CONFIG["extra_packages"]}" ]; then
build_args="$build_args --build-arg EXTRA_PACKAGES=\"${CONFIG["extra_packages"]}\""
fi
# 内存限制(防止 OOM)
local memory_limit="${BUILD_ARGS["memory_limit"]:-2048}"
if [ "$memory_limit" != "0" ]; then
build_args="$build_args --memory=${memory_limit}m"
fi
# 并行编译数
local parallel_jobs="${BUILD_ARGS["parallel_jobs"]:-0}"
if [ "$parallel_jobs" = "0" ]; then
parallel_jobs=$(nproc)
fi
build_args="$build_args --build-arg PARALLEL_JOBS=$parallel_jobs"
# 缓存设置
local enable_cache="${BUILD_ARGS["enable_cache"]:-true}"
if [ "$enable_cache" != "true" ]; then
build_args="$build_args --no-cache"
fi
# 复制本地扩展到构建上下文
if [ -d "$EXTENSION_DIR" ] && [ "$(ls -A "$EXTENSION_DIR" 2>/dev/null)" ]; then
mkdir -p "$BUILD_SCRIPTS_DIR/extensions"
cp -r "$EXTENSION_DIR"/* "$BUILD_SCRIPTS_DIR/extensions/" 2>/dev/null || true
fi
log_info "构建参数: $build_args"
# 执行构建
start_spinner "正在构建 Docker 镜像..."
# 修改判断逻辑,确保报错立即退出,不打印成功信息
if docker build $build_args -t "$image_tag" -f "$SCRIPT_DIR/Dockerfile" "$SCRIPT_DIR" 2>&1 | tee -a "$LOG_FILE"; then
stop_spinner "success" "Docker 镜像构建成功"
log_success "镜像构建完成: $image_tag"
else
# 只要 docker build 失败,进入这里
stop_spinner "fail" "Docker 镜像构建失败"
log_error "构建过程中发生错误,请查看上方日志或检查: $LOG_FILE"
exit 1 # 强制退出,不执行后续逻辑
fi
# 清理临时文件
if [ "${BUILD_ARGS["clean_cache"]}" = "true" ]; then
log_info "清理构建缓存..."
rm -rf "$BUILD_SCRIPTS_DIR/extensions" 2>/dev/null || true
fi
}
# ========================================
# 显示帮助信息
# ========================================
show_help() {
cat << EOF
${CYAN}${BOLD}PHP Hyperf Docker 构建工具${NC}
${YELLOW}用法:${NC}
./build.sh <镜像名称:标签>
./build.sh [选项]
${YELLOW}选项:${NC}
-h, --help 显示帮助信息
-v, --version 显示版本信息
-d, --debug 启用调试模式
--dry-run 仅解析配置,不执行构建
--validate 仅验证配置
${YELLOW}示例:${NC}
./build.sh myapp:latest
./build.sh myapp:v1.0.0
./build.sh --debug myapp:dev
${YELLOW}配置文件:${NC}
config/build.conf 主配置文件
config/extension_deps.conf 扩展依赖配置
config/extension_special.conf 扩展特殊配置
${YELLOW}本地扩展:${NC}
将扩展文件放入 extension/ 目录
支持格式: .tgz, .tar.gz, .zip
命名格式: 扩展名-版本号.tgz (如: swoole-4.8.13.tgz)
${YELLOW}日志文件:${NC}
logs/build_YYYYMMDD_HHMMSS.log
EOF
}
# ========================================
# 主函数
# ========================================
main() {
# 创建日志目录
mkdir -p "$LOGS_DIR"
# 参数解析
local image_tag=""
local dry_run=false
local validate_only=false
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
show_help
exit 0
;;
-v|--version)
echo "PHP Hyperf Docker Builder v1.0.0"
exit 0
;;
-d|--debug)
DEBUG=true
shift
;;
--dry-run)
dry_run=true
shift
;;
--validate)
validate_only=true
shift
;;
-*)
log_error "未知选项: $1"
show_help
exit 1
;;
*)
image_tag="$1"
shift
;;
esac
done
# 检查镜像标签
if [ -z "$image_tag" ] && [ "$validate_only" != true ]; then
log_error "请指定镜像名称和标签"
echo ""
show_help
exit 1
fi
echo ""
echo -e "${CYAN}${BOLD}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}${BOLD}║ PHP Hyperf Docker 构建工具 v1.0.0 ║${NC}"
echo -e "${CYAN}${BOLD}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""
log_info "日志文件: $LOG_FILE"
# 解析配置
parse_config
detect_image_type
parse_extension_deps
parse_extension_special
check_local_extensions
# 验证配置
validate_config
if [ "$validate_only" = true ]; then
log_success "配置验证完成"
exit 0
fi
# 生成安装脚本
generate_install_scripts
if [ "$dry_run" = true ]; then
log_success "Dry run 完成,脚本已生成到: $BUILD_SCRIPTS_DIR"
exit 0
fi
# 构建 Docker 镜像
#build_docker_image "$image_tag"
build_docker_image "$image_tag" || {
echo -e "${RED}构建失败,请检查上方日志${NC}"
exit 1
}
echo ""
log_success "构建完成!"
echo ""
echo -e "${GREEN}运行容器:${NC}"
echo -e " docker run -d --name hyperf -p 9501:9501 $image_tag"
echo ""
echo -e "${GREEN}查看日志:${NC}"
echo -e " docker logs -f hyperf"
echo ""
}
# 执行主函数
main "$@"
Dockerfile
# ========================================
# PHP Hyperf Docker 构建文件
# 所有参数通过 ARG 传入,不直接修改此文件
# ========================================
# 基础镜像参数
ARG BASE_IMAGE=php:8.2-cli
FROM ${BASE_IMAGE}
# 构建参数
ARG WORKDIR=/opt/www
ARG BUILD_MODE=prod
ARG PARALLEL_JOBS=4
ARG COMPOSER_MIRROR=auto
ARG TIMEZONE=Asia/Shanghai
# 环境变量
ARG ENV_VARS=""
ARG PHP_INI_CONFIG=""
ARG EXTRA_PACKAGES=""
ARG CMD_COMMAND=""
# 扩展安装脚本(由 build.sh 生成)
ARG INSTALL_DEPS_SCRIPT=""
ARG INSTALL_EXTENSIONS_SCRIPT=""
ARG POST_INSTALL_SCRIPT=""
# 标签
LABEL maintainer="PHP Hyperf Builder"
LABEL description="PHP Hyperf Application Container"
LABEL build.mode="${BUILD_MODE}"
# 设置工作目录
WORKDIR ${WORKDIR}
# 设置时区
ENV TZ=${TIMEZONE}
# 检测系统类型并安装基础依赖
RUN set -eux; \
# 检测是否为 Alpine
if [ -f /etc/alpine-release ]; then \
echo "Detected Alpine Linux"; \
apk update; \
apk add --no-cache \
bash \
curl \
wget \
git \
unzip \
tzdata \
$EXTRA_PACKAGES; \
cp /usr/share/zoneinfo/${TIMEZONE} /etc/localtime; \
echo "${TIMEZONE}" > /etc/timezone; \
else \
echo "Detected Debian/Ubuntu"; \
apt-get update; \
apt-get install -y --no-install-recommends \
bash \
curl \
wget \
git \
unzip \
ca-certificates \
$EXTRA_PACKAGES; \
ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime; \
echo "${TIMEZONE}" > /etc/timezone; \
fi
# 复制依赖安装脚本
COPY build_scripts/install_deps.sh /tmp/install_deps.sh
COPY build_scripts/install_extensions.sh /tmp/install_extensions.sh
COPY build_scripts/post_install.sh /tmp/post_install.sh
COPY build_scripts/install_composer.sh /tmp/install_composer.sh
# 安装系统依赖
RUN chmod +x /tmp/install_deps.sh && /tmp/install_deps.sh
# 安装 PHP 扩展
RUN chmod +x /tmp/install_extensions.sh && /tmp/install_extensions.sh
# 安装 Composer
RUN chmod +x /tmp/install_composer.sh && /tmp/install_composer.sh ${COMPOSER_MIRROR}
# 执行后置安装脚本
RUN chmod +x /tmp/post_install.sh && /tmp/post_install.sh
# 复制 PHP 配置
COPY build_scripts/php.ini /usr/local/etc/php/conf.d/99-custom.ini
# 清理
RUN set -eux; \
rm -rf /tmp/*; \
if [ -f /etc/alpine-release ]; then \
rm -rf /var/cache/apk/*; \
else \
apt-get clean; \
rm -rf /var/lib/apt/lists/*; \
fi; \
# 生产模式额外清理
if [ "${BUILD_MODE}" = "prod" ]; then \
rm -rf /usr/src/php*; \
rm -rf /usr/local/include/php; \
fi
# 设置环境变量
COPY build_scripts/env.sh /tmp/env.sh
RUN if [ -f /tmp/env.sh ]; then cat /tmp/env.sh >> /etc/profile; fi
# 暴露端口
#EXPOSE 9501
# 设置工作目录权限
RUN mkdir -p ${WORKDIR} && chmod -R 755 ${WORKDIR}
# 健康检查
#HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
# CMD curl -f http://localhost:9501/health || exit 1
# 入口点(可被覆盖)
#COPY build_scripts/entrypoint.sh /usr/local/bin/entrypoint.sh
#RUN chmod +x /usr/local/bin/entrypoint.sh
#ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
# CMD 命令(如果配置了)
# 这行会被 build.sh 动态添加
执行构建
# 进入项目目录
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,提升到此结束,又可以愉快的做牛马了 ... ... ...