--- 构建阶段 ---

阿乐
阿乐 管理员 年卡会员
发布于 2026-09-04 22:54 ·16 浏览 ·0 回复

从“天啊,这个PHP镜像居然快2个G了”到“嗯,这个镜像终于像样了”,中间隔着的其实就是几次恍然大悟的距离。

刚用Docker部署PHP项目时,我的思路非常简单粗暴:既然PHP和Nginx是最基础的运行环境,那就一条龙装齐。于是Dockerfile里不加思索地写着 `FROM php:8.2-apache`,然后 `apt-get install` 一堆扩展库,再 `docker-php-ext-install` 装上pdo_mysql、redis、opcache等。构建完成后对着镜像大小愣了半天——1.5GB,比一个完整操作系统还大。

交到生产环境时,部署要临时拉大容量,回滚时间长得让人坐立不安。那一刻我才意识到,镜像体积不只是一个存储数字,它直接牵着部署速度、带宽成本,往深层说,还关涉项目的交付流畅度。经过反复折腾,我总结了几条特别见效的瘦身路径。

选对基础镜像,放弃“全家桶”

PHP官方在Docker Hub提供多种变体,最常见的两个是 `php:8.2-apache`(基于Debian)和 `php:8.2-alpine`(基于Alpine Linux)。换用Alpine,镜像体积立马能从几百MB降到几十MB。

一个小对比:同一个空PHP环境,Debian版本镜像约400MB,Alpine版本约70MB。这多出来的300MB,在你自己的应用代码加入之前就已经白白堆上去了。

当然,Alpine并非没有缺点。它使用musl库而非glibc,一些自带二进制C扩展的PHP库(比如某些需要预编译的第三方SDK)可能不兼容。但如果你的项目运行的是常见PHP框架(Laravel、ThinkPHP、Symfony),Alpine几乎不会造成阻碍。对于极端精密的项目,也可以选用 `php:8.2-cli` + 单独Nginx容器的拆分方案,但多数场景不必做到这一步。

多阶段构建,把“造房子”和“入住”分开

真正让体积暴涨的,往往是安装了成堆“为了编译某扩展”的编译工具。以安装gd库为例,你可能需要 `libpng-dev`、`libjpeg-dev`,以及 `gcc`、`make` 等编译链。这些工具装完以后,运行时完全用不到,但又不能轻易删,因为删得不干净很可能导致扩展库依赖破裂。

多阶段构建(multi-stage build)恰好解决这个问题。思路很直白:用第一个阶段装好全套编译链和依赖,把扩展和依赖产物拿出来;第二阶段只基于一个干净的运行时基础镜像,把第一阶段编译好的东西拷贝过去。


FROM php:8.2-alpine AS builder

RUN apk add --no-cache $PHPIZE_DEPS \
    && docker-php-ext-install pdo_mysql opcache \
    && pecl install redis \
    && docker-php-ext-enable redis

# --- 运行阶段 ---
FROM php:8.2-alpine

# 只拷贝扩展,不拷贝编译工具
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/

WORKDIR /var/www/html
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY . .

作为中间层的builder镜像就算体积再大也没有关系,只要不推送到仓库,它只是构建过程的一个跳板,最终交付给运行的只有那个最小镜像。同理,node_modules安装、前端资源编译都可以前置到build阶段。

每一条指令都产生新的一层,干脆就少写几层

Dockerfile里每条会改变文件系统的指令,都会生成一个新的镜像层。层数多本身不是问题,问题在于每一层都会永久保存被删除或覆盖之前的文件记录——即使你在同一层里 `rm` 掉了一个大文件,磁盘上仍然保留着它之前存在过的痕迹。

常见的方案是把更新和清理压缩到同一条RUN里执行,让所有临时文件在同一个“layer生成周期”内消失:

RUN apk add --no-cache \
        freetype-dev \
        libjpeg-turbo-dev \
        libpng-dev \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install gd \
    && apk del freetype-dev libjpeg-turbo-dev libpng-dev

此外,记得给项目根目录写一个好的 `.dockerignore`——node_modules、源码里的.git目录、日志文件、临时缓存目录,这些被COPY进镜像往往是体积膨胀的核心来源。

把Composer依赖留在门外

刚入门时常犯的错是把composer安装完的目录整个打包,或者更糟——在镜像里运行composer install(既慢,又把网络依赖和metadata写进层的记录)。更好的做法是在CI阶段先通过composer install生成完整的vendor目录,然后通过.dockerignore排除掉composer.json和composer.lock需要的多余文件,把vendor直接COPY进镜像。

如果依赖的包里有特别大的(比如一些包含example、test、docs的包),还可以在构建时用 `composer install --no-dev --optimize-autoloader --prefer-dist` 来控制范围,配合 dump-autoload 的classmap也能进一步提升启动速度。

体积瘦身后的气象

这套组合拳配合下来,200MB的镜像能压到50MB以下,1GB级别的镜像直接掉到100-200MB之间。效果立竿见影:镜像拉取时间从分钟级降到秒级,k8s滚动更新的资源占用也顺滑了不少。

容器体积控制本质上是一种视角转换——镜像不该是另一个“运行环境快照”,而应该是“刚好够用的运行上下文”。该省的省、该精简的精简,构建慢的目的不是为了让镜像更好看,而是为了部署更快、迭代更稳。以后遇到“镜像怎么又卡了”的时刻,不妨静下心来数一数Dockerfile的层数,看看每一条COPY带进来的是什么——答案往往就在那些不起眼的行之间。

他们都看过 1 人浏览过
一只冷漠的狐狸

全部回复 0

还没有回复,来抢沙发~