PHP 应用的容器化早已不是新鲜事,但很多团队仍停留在“写个 Dockerfile 能跑就行”的阶段。结果就是镜像动辄 1GB 以上、构建缓存频繁失效、开发与生产环境行为不一致。本文从实际项目出发,梳理 PHP 与 Docker 结合时的三个关键环节:多阶段构建、环境配置管理、生产部署策略。
为什么 PHP 项目需要多阶段构建
传统 PHP 镜像通常基于 php:8.3-apache 或 php:8.3-fpm,直接在里面安装 Composer 依赖、Node 构建前端资源。这样做的问题很明显:
- 镜像体积大:Composer 缓存、Node 工具链、开发依赖全部留在最终镜像中
- 安全面扩大:构建工具和调试扩展在生产环境中毫无必要,却增加了攻击面
- 构建缓存利用率低:代码变更导致依赖层频繁重建
多阶段构建的核心思路是:在一个阶段完成依赖安装和资源编译,只把运行时真正需要的文件复制到最终镜像。
多阶段构建实战
以下是一个典型的 PHP 8.3 + FPM + Nginx 分离部署场景的 Dockerfile:
# ---------- Stage 1: Composer 依赖 ----------
FROM composer:2.7 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--no-interaction \
--no-scripts \
--prefer-dist \
--optimize-autoloader
# ---------- Stage 2: 前端资源构建 ----------
FROM node:20-alpine AS frontend
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY resources/ resources/
COPY vite.config.js ./
RUN npm run build
# ---------- Stage 3: 最终运行时镜像 ----------
FROM php:8.3-fpm-alpine
RUN apk add --no-cache \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
libzip-dev \
icu-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd zip intl opcache pdo_mysql
# OPcache 生产配置
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
WORKDIR /var/www/html
# 只复制运行时需要的文件
COPY --from=vendor /app/vendor/ ./vendor/
COPY --from=frontend /app/public/build/ ./public/build/
COPY . .
RUN chown -R www-data:www-data storage bootstrap/cache
USER www-data
EXPOSE 9000
CMD ["php-fpm"]
这个 Dockerfile 有几个值得注意的细节:
--no-scripts 与 --no-dev:在 vendor 阶段禁用脚本执行,避免构建时触发 Laravel 的 package discovery。同时不安装开发依赖,减少最终镜像中不必要的包。
Alpine 基础镜像:php:8.3-fpm-alpine 比 Debian 版本小约 60%,但需要注意某些扩展的编译依赖在 Alpine 上需要额外处理(比如 docker-php-ext-configure 的参数)。
USER www-data:最终镜像以非 root 用户运行,这是生产环境的基本安全要求。
分层复制:先复制 vendor 和编译产物,再复制应用代码。这样日常代码变更只会使最后一层失效,前面的依赖层仍然命中缓存。
环境配置:区分开发与生产
PHP 应用的环境配置通常通过 .env 文件管理,但 Docker 环境下需要更谨慎地处理。
不要将 .env 打入镜像
.env 包含数据库密码、API 密钥等敏感信息,绝对不应该出现在镜像层中。正确做法是通过运行时注入:
# docker-compose.prod.yml
services:
app:
image: registry.example.com/myapp:${TAG}
env_file:
- .env.production
environment:
- APP_ENV=production
- APP_DEBUG=false
在 .dockerignore 中明确排除:
.env
.env.*
!.env.example
利用 PHP 的配置文件分层
PHP 官方镜像支持在 /usr/local/etc/php/conf.d/ 目录下放置 .ini 文件,按文件名排序加载。可以针对不同环境准备不同的配置:
; docker/php/opcache.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.validate_timestamps=0 在生产环境中关闭文件时间戳检查,避免每次请求都 stat 文件。代价是代码更新后需要重启 PHP-FPM,这在容器化部署中本来就是标准操作。
构建参数 vs 运行时环境变量
一个常见的误区是把所有配置都做成构建参数(ARG)。实际上只有那些影响编译结果的配置才应该用 ARG,比如:
ARG APP_ENV=production
RUN if [ "$APP_ENV" = "production" ]; then \
composer install --no-dev --optimize-autoloader; \
else \
composer install; \
fi
而数据库连接、缓存驱动、队列配置等运行时才确定的值,应该通过环境变量注入。
生产部署策略
镜像标签管理
永远不要在生产环境使用 latest 标签。推荐使用 Git commit SHA 或语义化版本号:
docker build -t registry.example.com/myapp:$(git rev-parse --short HEAD) .
docker push registry.example.com/myapp:$(git rev-parse --short HEAD)
零停机部署
PHP-FPM 的优雅重启是实现零停机的关键。在 Kubernetes 或 Docker Swarm 中,利用滚动更新策略:
# Kubernetes Deployment 片段
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
配合 preStop 钩子确保正在处理的请求完成:
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5 && kill -QUIT 1"]
健康检查
为 PHP-FPM 容器添加健康检查,确保流量只路由到就绪的实例:
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD php-fpm-healthcheck || exit 1
需要安装 php-fpm-healthcheck 脚本,或者简单使用 cgi-fcgi 探测:
HEALTHCHECK CMD SCRIPT_NAME=/ping SCRIPT_FILENAME=/ping \
REQUEST_METHOD=GET cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1
日志处理
容器化环境中,PHP-FPM 应该将日志输出到 stdout/stderr,由容器运行时统一收集:
; docker/php/fpm-pool.conf
[www]
catch_workers_output = yes
decorate_workers_output = no
php_admin_value[error_log] = /proc/self/fd/2
php_admin_flag[log_errors] = on
这样日志可以直接被 docker logs、Fluentd 或 Loki 采集,无需在容器内管理日志文件轮转。
小结
PHP 与 Docker 的结合,核心在于三点:用多阶段构建控制镜像体积和攻击面,用运行时注入管理环境配置,用滚动更新和健康检查保障生产部署的稳定性。这些实践并不复杂,但需要在项目初期就建立规范,否则后期迁移成本会成倍增加。
未经允许不得转载:任鹏个人博客 » PHP 与 Docker:多阶段构建、环境配置与生产部署实战


朋友圈点赞图在线生成源码