
一个只装 PHP 运行时的镜像本该不到 200MB,实际却常常超过 1GB,原因多半是编译工具链与构建缓存被一层层留在了镜像里。Docker 多阶段构建允许在同一个 Dockerfile 里写多个 FROM,只有最后一个阶段进入最终镜像,前面阶段只负责产出文件。
一、单阶段写法为什么把镜像体积撑大
1. 构建依赖和运行依赖混在一起
编译 PHP 扩展需要 gcc、make、libssl-dev,运行时完全用不到。而 Docker 镜像分层只增不减:同一层里装了再删,体积依然算在这一层。
FROM php:8.2-fpm
RUN apt-get update && apt-get install -y gcc make libssl-dev
仅这一层就多出约 400MB 编译工具,后面执行 apt-get purge,体积也不会回落。
2. Composer 缓存与 vendor 目录重复
COPY . /app
RUN composer install --no-dev
composer 的下载缓存默认在 ~/.composer/cache 且不自动清理;COPY . 还会把本地 vendor 一起带进去,造成两份依赖,单是 vendor 就可能占 60 到 120MB。
二、多阶段构建的完整写法
1. 用 as 命名阶段,只 COPY 产物
思路是把构建和运行彻底拆开,运行阶段只保留运行时和产物。
# 构建阶段:只产出文件
FROM composer:2 AS builder
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist
# 运行阶段:最终镜像只基于它
FROM php:8.2-fpm
WORKDIR /app
COPY --from=builder /app/vendor ./vendor
COPY . .
CMD ["php-fpm"]
COPY --from=builder 只搬运指定目录,builder 阶段的层不会进入最终镜像。
2. 合并 RUN 并清理包管理器缓存
apt 与 composer 的缓存必须在同一层内删除才有效,要用 && 串在一条 RUN 里,分开写会让前一层的缓存永久残留。
RUN apt-get update \
&& apt-get install -y --no-install-recommends libssl3 \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
三、上线前的校验与常见坑
1. 用 docker history 定位异常大层
docker history --no-trunc --human app:latest | head -20
按 SIZE 从大到小看,优先削掉占比最高的那一层,通常就是漏清理的 apt 或 composer。
2. .dockerignore 与构建缓存的取舍
构建缓存命中依赖文件内容与拷贝顺序。把 composer.json 与 composer.lock 单独先 COPY 再 install,只要依赖清单不变,改业务代码时这一步依然命中。同时必须写 .dockerignore 排除无谓目录,否则 COPY . . 会把它们带进镜像并打穿缓存。
.git
node_modules
vendor
.env
关键配置提示
- 构建阶段直接用官方 composer:2 镜像,--prefer-dist 走压缩包比 --prefer-source 快很多。
- 改完 Dockerfile 后用 --no-cache 与默认缓存各构建一次,对比体积差异,确认清理确实生效。

快来评论,快来抢沙发吧~