1. 为什么要在官方 PHP 镜像上做二次构建
1.1 从一次真实需求说起
去年接手一个老项目迁移,代码是典型的 PHP 5 时代风格,跑在物理机上,用了大量 GD 库相关的图片处理逻辑——验证码生成、缩略图裁剪、水印叠加,全是imagecreatefromjpeg、imagecopyresampled这类函数。迁移目标是把整套环境容器化,基础镜像选型时我直接拉了php:7.3.4-fpm,理由很简单:版本号锁死,和原环境一致,避免运行时行为差异。
结果容器一跑起来,验证码接口直接 500。日志里一行Call to undefined function imagecreatefrompng()把问题指得很明白——官方这个镜像默认没编译 GD 扩展。这不是什么冷门坑,但第一次踩的人容易懵:明明本地开发环境php -m能看到 gd,怎么容器里就没了?
原因在于官方 PHP 镜像走的是极简路线。它只编译了最核心的扩展,像 GD、mysqli、bcmath、exif 这些"常用但非必需"的模块,全部留给你自己按需安装。这个设计其实很合理,镜像体积能压到 70MB 出头,但代价就是你必须掌握在官方镜像基础上二次构建的能力。
1.2 这个内容适合谁看
如果你正在做下面这些事,这篇东西应该能帮上忙:
- 手上有个 PHP 项目要容器化,但不确定官方镜像缺哪些扩展
- 已经遇到 GD 缺失报错,想搞清楚怎么补、补哪些依赖
- 想理解 Dockerfile 里
docker-php-ext-install和docker-php-ext-configure到底在干什么 - 需要给镜像瘦身,不想用那种"什么都装好"的臃肿基础镜像
我下面会从镜像选型逻辑讲起,把 GD 扩展的编译依赖、配置参数、构建步骤、验证方法、常见报错全部拆开说。所有操作都在php:7.3.4-fpm这个具体版本上验证过,但思路可以平移到其他 PHP 版本。
2. 镜像选型与 GD 扩展的依赖关系
2.1 为什么是 php:7.3.4-fpm 而不是 alpine
官方 PHP 镜像有好几个变体,选哪个直接决定后面装扩展的难度。我整理了一张对比表,都是实际构建时踩出来的经验:
| 镜像标签 | 基础系统 | 体积 | 装 GD 难度 | 适用场景 |
|---|---|---|---|---|
php:7.3.4-fpm | Debian | 约 150MB | 低,apt 装依赖 | 生产环境首选 |
php:7.3.4-fpm-alpine | Alpine | 约 80MB | 中,apk 装依赖 | 追求极致体积 |
php:7.3.4-cli | Debian | 约 130MB | 低 | 命令行脚本 |
php:7.3.4-apache | Debian | 约 180MB | 低 | 需要 Apache |
选php:7.3.4-fpm的核心理由是 Debian 的包管理生态成熟,libpng-dev、libjpeg-dev、libfreetype6-dev这些 GD 编译依赖一个apt-get install就能搞定,不用折腾 Alpine 的 musl libc 兼容问题。Alpine 镜像虽然小,但编译 GD 时经常遇到freetype头文件路径不对、jpeg库链接失败这类问题,排查成本高。
提示:如果你的项目对镜像体积极度敏感,Alpine 方案确实能省 70MB 左右,但建议预留半天时间处理编译依赖。生产环境如果不是特别缺这点空间,Debian 系更省心。
2.2 GD 扩展到底依赖哪些库
GD 不是一个孤立的扩展,它是一层封装,底层调用的是系统里的图形库。要支持常见的图片格式,需要这几个:
- libpng-dev:处理 PNG 格式,
imagecreatefrompng靠它 - libjpeg-dev:处理 JPEG 格式,
imagecreatefromjpeg靠它 - libfreetype6-dev:处理 TrueType 字体,
imagettftext写文字靠它 - libwebp-dev:处理 WebP 格式(可选,但现代项目建议加上)
这里有个容易忽略的点:-dev后缀不能省。libpng是运行时库,libpng-dev才包含编译需要的头文件。只装运行时库的话,docker-php-ext-configure阶段会报找不到png.h。
2.3 docker-php-ext-install 和 docker-php-ext-configure 的分工
官方镜像内置了两个脚本,理解它们的分工是构建成功的关键:
docker-php-ext-configure:在编译前设置扩展的配置参数,比如 GD 要指定--with-freetype-dir和--with-jpeg-dir的路径docker-php-ext-install:执行实际的下载、编译、安装流程,把扩展装进 PHP 的扩展目录
GD 的特殊之处在于它必须先用configure指定依赖库路径,否则编译时会用默认路径去找,找不到就静默跳过对应格式的支持。我见过有人只跑install不跑configure,结果 GD 装上了但不支持 JPEG,排查半天才发现是配置阶段的问题。
3. 完整 Dockerfile 编写与参数详解
3.1 一份可直接用的 Dockerfile
先上完整文件,然后逐段拆解:
FROM php:7.3.4-fpm # 替换为国内镜像源,加速 apt 下载 RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \ && sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list # 安装 GD 编译所需的系统依赖 RUN apt-get update && apt-get install -y \ libpng-dev \ libjpeg-dev \ libfreetype6-dev \ libwebp-dev \ && rm -rf /var/lib/apt/lists/* # 配置并安装 GD 扩展 RUN docker-php-ext-configure gd \ --with-freetype-dir=/usr/include/freetype2 \ --with-jpeg-dir=/usr/include \ --with-webp-dir=/usr/include \ && docker-php-ext-install -j$(nproc) gd # 验证扩展是否加载 RUN php -m | grep -i gd这份文件我用了很多次,构建成功率很高。下面把每个决策点讲清楚。
3.2 换源这一步为什么不能省
php:7.3.4-fpm基于 Debian 9(Stretch),默认 apt 源指向deb.debian.org。国内构建时这一步经常卡住或者超时,导致整个docker build失败。换成国内镜像源后,apt-get update从几十秒降到几秒。
注意:换源命令里的
sed要同时处理sources.list里的两处地址,一处是主源,一处是安全更新源。只改一处的话,apt-get update仍可能因为安全源超时而失败。
如果你构建环境本身网络通畅,这步可以去掉。但作为通用模板,我建议保留,反正不影响功能。
3.3 依赖安装的合并与清理
apt-get install和rm -rf /var/lib/apt/lists/*写在同一个RUN里,这是 Docker 分层机制决定的。每条RUN产生一个镜像层,如果把安装和清理分成两条RUN,清理操作只是在新层里删文件,前一层里的 apt 缓存依然占着体积。合并成一条,缓存文件在同一个层里被删掉,最终镜像不会包含它们。
-j$(nproc)这个参数是让编译并行化,用满 CPU 核心数。GD 扩展编译本身不慢,但加上这个参数能再快几秒,属于顺手优化。
3.4 configure 参数里的路径怎么确定
--with-freetype-dir、--with-jpeg-dir、--with-webp-dir这三个路径不是随便写的,得看依赖库实际装在哪。Debian 系统里:
- freetype 头文件在
/usr/include/freetype2 - jpeg 头文件在
/usr/include - webp 头文件在
/usr/include
如果你不确定,可以在装完依赖后进容器跑dpkg -L libfreetype6-dev | grep freetype.h来确认。路径写错的话,configure 阶段会警告某个格式不支持,但不会直接报错退出,所以一定要看构建日志里的 warning。
提示:PHP 7.3 的 GD 配置参数用的是
--with-freetype-dir,到了 PHP 7.4 之后改成了--with-freetype,路径写法也不同。跨版本迁移时这个参数要改,否则 configure 会失败。
4. 构建、验证与镜像瘦身
4.1 构建命令与构建缓存利用
docker build -t php:7.3.4-fpm-gd .构建时 Docker 会按 Dockerfile 顺序逐层执行。第一次构建会下载基础镜像、装依赖、编译扩展,大概需要 2-3 分钟。之后如果只改了业务代码没动 Dockerfile,这些层会直接命中缓存,秒级完成。
我习惯把不常变的部分(换源、装依赖)放前面,把可能频繁调整的部分(比如额外装其他扩展)放后面,这样改后面内容时前面的缓存还能用。
4.2 验证 GD 是否真正可用
构建完别急着用,先验证。最直接的方法是进容器看扩展列表:
docker run --rm php:7.3.4-fpm-gd php -m | grep -i gd输出gd就说明扩展加载了。但这还不够,因为 GD 可能装上了却不支持某些格式。更严格的验证是看 GD 的详细信息:
docker run --rm php:7.3.4-fpm-gd php -r "print_r(gd_info());"输出里会列出JPEG Support、PNG Support、FreeType Support、WebP Support这些字段,值为 1 表示支持。我实测下来,按上面的 Dockerfile 构建,这几项应该全是 1。
如果只想快速验证图片处理能力,可以跑一段实际代码:
docker run --rm php:7.3.4-fpm-gd php -r " \$img = imagecreatetruecolor(100, 100); \$color = imagecolorallocate(\$img, 255, 0, 0); imagefilledrectangle(\$img, 0, 0, 100, 100, \$color); imagepng(\$img, '/tmp/test.png'); echo 'PNG created: ' . (file_exists('/tmp/test.png') ? 'yes' : 'no'); "这段代码创建一张纯红图片并保存为 PNG,能跑通说明 GD 的核心功能没问题。
4.3 镜像体积优化
按上面的 Dockerfile 构建出来的镜像大概 200MB 左右,比基础镜像多了 50MB。多出来的部分主要是编译依赖和 apt 缓存。如果想进一步瘦身,有几个办法:
- 装完依赖编译完扩展后,用
apt-get purge删掉-dev包,再apt-get autoremove - 用多阶段构建,编译阶段装依赖,运行阶段只拷贝编译好的
.so文件 - 直接换 Alpine 基础镜像
多阶段构建的写法大概是这样:
FROM php:7.3.4-fpm AS builder RUN apt-get update && apt-get install -y libpng-dev libjpeg-dev libfreetype6-dev libwebp-dev RUN docker-php-ext-configure gd --with-freetype-dir=/usr/include/freetype2 --with-jpeg-dir=/usr/include --with-webp-dir=/usr/include \ && docker-php-ext-install -j$(nproc) gd FROM php:7.3.4-fpm COPY --from=builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/ COPY --from=builder /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/这样最终镜像里不含编译工具和-dev包,体积能压到 160MB 左右。代价是 Dockerfile 复杂一些,构建时间稍长。
注意:多阶段构建拷贝扩展目录时,要确保两个阶段的基础镜像完全一致,否则扩展的 ABI 可能不兼容。这里两个阶段都用
php:7.3.4-fpm,所以没问题。
5. 常见报错与排查实录
5.1 构建阶段报错
报错一:configure: error: png.h not found
原因:没装libpng-dev,或者装了但路径不对。解决方法是确认apt-get install列表里有libpng-dev,并且 configure 时不需要额外指定 png 路径(GD 会自动找)。
报错二:configure: error: freetype.h not found
原因:libfreetype6-dev没装,或者--with-freetype-dir路径写错。Debian 系统里正确路径是/usr/include/freetype2,不是/usr/include。
报错三:docker-php-ext-install: command not found
原因:基础镜像不是官方 PHP 镜像,或者用了精简版。docker-php-ext-install是官方镜像特有的脚本,换其他基础镜像就没有了。
5.2 运行阶段报错
报错四:Call to undefined function imagecreatefromjpeg()
原因:GD 装了但不支持 JPEG。大概率是 configure 时--with-jpeg-dir没写或写错。用gd_info()确认JPEG Support是否为 1。
报错五:Call to undefined function imagettftext()
原因:FreeType 支持没编译进去。检查--with-freetype-dir路径,以及libfreetype6-dev是否安装。
报错六:imagepng(): gd-png: fatal libpng error: Invalid filter type
原因:libpng 版本和 GD 编译时的版本不匹配。这种情况少见,通常出现在手动拷贝扩展文件时。解决办法是重新构建,确保编译和运行环境一致。
5.3 排查速查表
| 现象 | 可能原因 | 排查命令 | 解决 |
|---|---|---|---|
| 构建时 png.h 找不到 | 缺 libpng-dev | dpkg -l | grep libpng | 装 libpng-dev |
| 构建时 freetype.h 找不到 | 路径写错 | dpkg -L libfreetype6-dev | grep freetype.h | 改 configure 路径 |
| GD 装了但不支持 JPEG | configure 缺 jpeg 参数 | php -r "print_r(gd_info());" | 补 --with-jpeg-dir |
| 扩展加载失败 | .so 文件 ABI 不兼容 | php -m看报错 | 重新构建,统一基础镜像 |
| 镜像体积过大 | 编译依赖没清理 | docker history看层大小 | 多阶段构建或 purge |
5.4 几个我踩过的坑
第一个坑是换源后忘了apt-get update。sed改完sources.list只是改了地址,包索引还是旧的,直接install会报找不到包。必须update一次。
第二个坑是docker-php-ext-configure和docker-php-ext-install写成了两条独立的RUN。这样也能构建成功,但 configure 的配置在下一层里可能丢失,导致扩展编译时用了默认配置。稳妥做法是写在同一层里,用&&连接。
第三个坑是验证时只看了php -m有 gd 就以为万事大吉。实际上php -m只说明扩展加载了,不说明格式支持完整。一定要跑gd_info()看具体支持项。
6. 扩展思路与生产环境建议
6.1 把同样的方法用到其他扩展上
GD 的安装套路可以套用到大部分官方镜像没带的扩展上,比如mysqli、bcmath、exif、intl。区别只在于依赖库和 configure 参数不同:
mysqli:不需要额外系统依赖,直接docker-php-ext-install mysqlibcmath:同样不需要依赖,直接 installintl:需要libicu-dev,configure 时指定--with-icu-direxif:不需要依赖,直接 install
理解了"装依赖 → configure → install → 验证"这个流程,换任何扩展都是查一下依赖库和参数的事。
6.2 生产环境的几个建议
第一,镜像标签别用latest。php:7.3.4-fpm这种锁死版本号的标签,能保证每次构建拉到的都是同一个基础镜像。用latest的话,某天基础镜像更新了,你的构建结果可能就变了。
第二,把构建好的镜像推到私有仓库,别每次部署都重新构建。构建过程依赖网络下载 apt 包,生产环境不一定有外网。推仓库后部署直接拉镜像,快且稳定。
第三,如果项目用了多个扩展,建议写一个统一的 Dockerfile 一次装完,而不是基于一个装好 GD 的镜像再装别的。多层继承会让镜像层数变多,体积也更大。
第四,定期检查基础镜像的安全更新。php:7.3.4-fpm这个版本已经比较老了,如果项目允许,建议升级到 7.3 的更高补丁版本或者 7.4,安全性和性能都有提升。升级时注意 GD 的 configure 参数在 7.4 有变化,前面提过。
6.3 关于 PHP 7.3 的现状
PHP 7.3 在 2021 年底就停止官方支持了,不再有安全补丁。如果这是新项目,强烈建议直接用 7.4 或 8.x。但现实里很多老项目因为代码兼容性问题被锁在 7.3,这种情况下容器化时更要注重镜像的安全基线,比如定期重建镜像拉取系统层的最新安全更新,或者用一些镜像扫描工具检查已知漏洞。
我自己维护的几个 7.3 项目,做法是每季度重建一次镜像,apt-get update会拉到 Debian 的最新安全补丁,虽然 PHP 本身不更新了,但系统层能补一点是一点。同时把项目里用不到的扩展全部去掉,减少攻击面。
这套在官方镜像上加 GD 的方法,我从 7.2 时代一直用到 8.x,核心逻辑没变过,变的只是 configure 参数和依赖包名。掌握一次,后面都是查文档的事。