PHP 是 Web 开发中最流行的语言之一。容器化 PHP 应用通常采用 Nginx + PHP-FPM 的架构:Nginx 作为 Web 服务器处理 HTTP 请求和静态文件,并将动态 PHP 请求通过 FastCGI 协议转发给 PHP-FPM 进程执行。
背景介绍
PHP 应用的容器化有其独特特点和挑战:
- 解释型语言:PHP 代码需要解释器执行,不同于 Go 的静态编译
- Web 服务架构:典型的 PHP 应用需要 Web 服务器与 PHP 解释器配合
- 依赖管理:通过 Composer 管理 PHP 库依赖,类似于 npm、pip 等
- 多进程模型:PHP-FPM 使用进程池管理请求,需要合理配置以优化性能
本篇将指导你如何使用多阶段构建和单容器模式(使用 Supervisor 同时管理 Nginx 和 PHP-FPM)来构建和运行一个现代化的 PHP 应用。我们将基于上一篇构建的 Debian 基础镜像,遵循逻辑分层设计原则。
构建 PHP 工具环境镜像
PHP 工具环境负责提供完整的 PHP 运行时和相关组件,是后续应用运行的基础。
创建镜像目录
首先创建 PHP 工具环境镜像的目录:
mkdir -p common/tools/php
cd common/tools/php
PHP 工具环境 Dockerfile 详解
下面是 PHP 环境的 Dockerfile,它安装了特定版本的 PHP-FPM 以及常用扩展和工具:
#syntax=harbor.leops.local/library/docker/dockerfile:1
FROM harbor.leops.local/common/os/debian:bullseye
ARG PHP_VERSION=8.4
LABEL org.opencontainers.image.authors="ops@leops.local" \
org.opencontainers.image.source="http://git.leops.local/ops/dockerfiles-base/common/tools/php/Dockerfile" \
org.opencontainers.image.description="php ${PHP_VERSION} compiler environment."
ENV PHP_VERSION=$PHP_VERSION
# install dependencies
RUN set -eux; \
echo "deb [trusted=yes] https://packages.sury.org/php/ bullseye main" > /etc/apt/sources.list.d/php.list; \
echo "deb [trusted=yes] https://packages.microsoft.com/debian/11/prod bullseye main" > /etc/apt/sources.list.d/mssql-release.list; \
apt-get update; \
apt-get install -y --no-install-recommends \
zip \
mcrypt \
libmemcached-dev \
libreadline-dev \
libmcrypt-dev \
libreadline-dev \
php${PHP_VERSION}-fpm \
php${PHP_VERSION}-cli \
php${PHP_VERSION}-curl \
php${PHP_VERSION}-intl \
php${PHP_VERSION}-mbstring \
php${PHP_VERSION}-xsl \
php${PHP_VERSION}-dev \
php${PHP_VERSION}-zip \
php${PHP_VERSION}-mcrypt \
php${PHP_VERSION}-bcmath \
php${PHP_VERSION}-bz2 \
php${PHP_VERSION}-gd \
php${PHP_VERSION}-igbinary \
php${PHP_VERSION}-imagick \
php${PHP_VERSION}-imap \
php${PHP_VERSION}-ldap \
php${PHP_VERSION}-memcache \
php${PHP_VERSION}-mongo \
php${PHP_VERSION}-mysql \
php${PHP_VERSION}-redis \
php${PHP_VERSION}-sqlite3 \
php${PHP_VERSION}-pgsql \
php${PHP_VERSION}-xml \
php${PHP_VERSION}-xmlrpc \
php${PHP_VERSION}-readline \
php${PHP_VERSION}-gmp \
php${PHP_VERSION}-soap \
php${PHP_VERSION}-ssh2 \
php${PHP_VERSION}-xdebug \
php${PHP_VERSION}-xhprof \
php${PHP_VERSION}-swoole \
ghostscript \
php-pear; \
ACCEPT_EULA=Y apt-get install -y msodbcsql17 unixodbc-dev libgssapi-krb5-2; \
sed -i 's#rights="none" pattern="PDF"#rights="read|write" pattern="PDF"#g' /etc/ImageMagick-*/policy.xml; \
mkdir -p /run/php /var/lib/php/wsdlcache /var/lib/php/session; \
chown -R www-data /run/php /var/lib/php/wsdlcache /var/lib/php/session; \
curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php; \
php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer; \
composer --version; \
composer config -g repo.packagist composer http://nexus.leops.local/repository/composer/; \
composer config -g secure-http false; \
composer config -gl; \
apt-get clean; \
rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* /tmp/* /var/tmp/*; \
truncate -s 0 /var/log/*log;
CMD ["/bin/bash", "-c", "/usr/sbin/php-fpm${PHP_VERSION} --nodaemonize --fpm-config=/etc/php/${PHP_VERSION}/fpm/pool.d/www.conf"]
这个 PHP 环境 Dockerfile 有以下几个重要特点:
- 基于 Debian:使用了我们之前创建的标准化 Debian 基础镜像
- PPA 源配置:添加了 PHP 和 Microsoft 的软件源,获取最新版本的 PHP 和 MSSQL 驱动
- 丰富的扩展支持:安装了常用的 PHP 扩展(mysql、redis、curl、gd 等)
- Composer 集成:安装并配置了 PHP 的包管理工具 Composer
- 权限管理:为 PHP-FPM 创建必要的目录并设置正确的权限
- 镜像仓库代理:配置 Composer 使用内部镜像仓库,加速依赖下载
- 缓存清理:每个步骤后清理不必要的缓存文件,减小镜像体积
其中 composer 的 repo.packagist 使用了我们之前搭建的 nexus 代理,在 nexus 上创建了一个 composer proxy , Remote storage 是 https://mirrors.aliyun.com/composer/
镜像构建脚本
使用以下脚本 (build.sh) 来构建和推送 PHP 工具环境镜像:
#!/bin/bash
set -e
# 配置
REGISTRY="harbor.leops.local"
IMAGE_BASE_NAME="common/tools/php"
VERSION="8.4"
# 声明镜像地址数组
declare -a IMAGE_PATHS
IMAGE_PATHS+=(
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION}"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION%.*}"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION}-debian11"
"${REGISTRY}/${IMAGE_BASE_NAME}:${VERSION%.*}-debian11"
)
build_image() {
echo "Building and pushing image:"
for img in "${IMAGE_PATHS[@]}"; do echo -n " $img"; done
# 构建镜像
docker buildx build \
$(for img in "${IMAGE_PATHS[@]}"; do echo -n "-t $img "; done) \
--label "org.opencontainers.image.created=$(date --rfc-3339=seconds)" \
--build-arg "PHP_VERSION=${VERSION}" \
--provenance=false \
--pull \
--push \
.
echo "Build complete."
}
# 参数处理
case "$1" in
"list-tags")
# 输出镜像标签列表
printf '%s\n'"${IMAGE_PATHS[@]}"
;;
*)
build_image
;;
esac
这个脚本支持创建多个标签版本,包括完整版本号、主版本号以及带有系统标识的组合标签,确保镜像引用的灵活性。
构建 Yii 框架运行环境镜像
在 PHP 工具环境基础上,我们需要构建特定框架的运行环境镜像。这里以 Yii[1] 框架为例,它是一个高性能的 PHP 框架,用于开发现代 Web 应用程序。
为什么需要专门的框架运行环境?
框架专用运行环境的优势:
- 预配置组件:包含框架所需的所有必要组件(Nginx、PHP-FPM、Supervisor 等)
- 标准化配置:针对框架特性优化的服务配置文件
- 监控和管理:集成了监控和管理工具,如 PHP-FPM Exporter
- 流程自动化:通过启动脚本自动适配不同环境的配置需求
创建框架运行环境目录
mkdir -p common/runtime/yii
cd common/runtime/yii
Yii 框架运行环境 Dockerfile
下面是一个基于 php-fpm 的应用运行镜像示例:
#syntax=harbor.leops.local/library/docker/dockerfile:1
ARG PHP_VERSION=8.4
FROM harbor.leops.local/common/tools/php:${PHP_VERSION}
ARG PHP_VERSION=8.4
LABEL org.opencontainers.image.authors="ops@leops.local" \
org.opencontainers.image.source="http://git.leops.local/ops/dockerfiles-base/common/runtime/yii/Dockerfile" \
org.opencontainers.image.description="Yii runtime environment."
# install dependencies
RUN set -eux; \
echo 'deb [trusted=yes] https://nginx.org/packages/debian/ bullseye nginx' > /etc/apt/sources.list.d/nginx.list; \
apt-get update; \
apt-get install -y --no-install-recommends \
nginx \
supervisor; \
apt-get clean; \
rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* /tmp/* /var/tmp/*; \
truncate -s 0 /var/log/*log;
# Install CacheTool
RUN curl -s -L https://github.com/gordalina/cachetool/releases/download/7.1.0/cachetool.phar -o /opt/cachetool.phar \
&& chmod +x /opt/cachetool.phar \
&& echo 'cachetool(){ php /opt/cachetool.phar --fcgi=127.0.0.1:9000 --tmp-dir=/tmp $@;}' >> /etc/bash.bashrc
COPY --from=hipages/php-fpm_exporter:2.2.0 /php-fpm_exporter /usr/local/bin/
COPY etc /etc
COPY scripts /scripts
WORKDIR /app
CMD ["/scripts/entrypoint.sh"]
配置文件详解
框架运行环境包含多个关键配置文件,下面是一些重要的配置文件:
Nginx 配置文件 (etc/nginx/nginx.conf)
user www-data;
pid /run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 65535;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 10240;
use epoll;
}
http {
log_format main_json '{"@timestamp": "$time_iso8601", '
'"remote_addr": "$remote_addr", '
'"Remoteip": "$http_Remoteip", '
'"realip_remote_addr": "$realip_remote_addr", '
'"http_x_forwarded_for": "$http_x_forwarded_for", '
'"scheme": "$scheme", '
'"request_method": "$request_method", '
'"host": "$host", '
'"request_uri": "$request_uri", '
'"body_bytes_sent": $body_bytes_sent, '
'"http_referer": "$http_referer", '
'"http_user_agent": "$http_user_agent", '
'"upstream_addr": "$upstream_addr", '
'"request_time": $request_time, '
'"request_length": $request_length, '
'"cookie_staff_id": "$cookie_staff_id", '
'"cookie_login_name": "$cookie_login_name", '
'"upstream_connect_time": "$upstream_connect_time", '
'"upstream_header_time": "$upstream_header_time", '
'"upstream_response_time": "$upstream_response_time", '
'"upstream_status": "$upstream_status", '
'"status": "$status"}';
access_log /dev/stdout main_json;
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
max_ranges 1;
tcp_nodelay on;
keepalive_timeout 180;
send_timeout 180;
proxy_ignore_client_abort on;
reset_timedout_connection on;
open_file_cache max=65535 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 5;
open_file_cache_errors off;
client_header_timeout 180;
client_body_timeout 180;
client_max_body_size 100m;
client_body_buffer_size 100M;
client_header_buffer_size 320k;
large_client_header_buffers 4320k;
fastcgi_connect_timeout 300s;
fastcgi_send_timeout 300s;
fastcgi_read_timeout 300s;
fastcgi_buffer_size 128k;
fastcgi_buffers 8128k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
gzip on;
gzip_min_length 1k;
gzip_buffers 416K;
gzip_comp_level 3;
gzip_types application/x-javascript text/css application/javascript text/javascript text/plain text/xml application/json application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/xml font/eot font/opentype font/otf image/svg+xml image/vnd.microsoft.icon;
gzip_disable "MSIE [1-6]\.";
server {
charset utf-8;
listen 80;
server_name default;
root /app/${APP}/web;
index index.html index.php index.htm;
set $yii_bootstrap "index.php";
location ~ \.(jpg|png|jpeg|bmp|gif|swf|css|js|pdf|ico|woff|tff)$
{
access_log off;
expires 30d;
}
location / {
try_files $uri $uri/ /$yii_bootstrap?$args;
}
location ~ ^/(protected|framework|themes/\w+/views) {
deny all;
}
location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ {
try_files$uri =404;
}
location ~ \.php {
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
fastcgi_split_path_info ^(.+\.php)(.*)$;
set$fsn /$yii_bootstrap;
if (-f $document_root$fastcgi_script_name){
set $fsn $fastcgi_script_name;
}
fastcgi_pass 127.0.0.1:9000;
#fastcgi_pass unix:/var/run/php-fpm/php-fpm.socket;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fsn;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fsn;
fastcgi_param PHP_VALUE open_basedir=/tmp:/app;
fastcgi_param REDIRECT_STATUS 200;
fastcgi_param APP_ENV ${APP_ENV};
}
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location /_/nginx_status {
stub_statuson;
}
location ~ ^/_/php_fpm_(status|ping)$ {
allow 127.0.0.1;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
#fastcgi_pass unix:/var/run/php-fpm/php-fpm.socket;
}
}
}
Supervisor 配置文件 (etc/supervisor/supervisord.conf)
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
username=www-data
password=www-data
[supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10 ; (num of main logfile rotation backups;default 10)
loglevel=info ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false ; (start in foreground if true;default false)
minfds=10240 ; (min. avail startup file descriptors;default 1024)
minprocs=200 ; (min. avail process descriptors;default 200)
user=root ; (default is current user, required if root)
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock
[program:php-fpm]
command=/usr/sbin/php-fpm8.4 --nodaemonize --fpm-config=/etc/php/8.4/fpm/php-fpm.conf
autostart=true
autorestart=true
priority=5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;error_log /dev/stderr debug;"
autostart=true
autorestart=true
priority=10
stdout_events_enabled=true
stderr_events_enabled=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:php-fpm-exporter]
command=/usr/local/bin/php-fpm_exporter server --phpfpm.scrape-uri tcp://127.0.0.1:9000/_/php_fpm_status --web.listen-address :9091
autostart=true
autorestart=true
priority=10
stdout_events_enabled=true
stderr_events_enabled=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
启动脚本 (scripts/entrypoint.sh)
#!/bin/bash
sed -i -e "s/worker_processes auto/worker_processes ${NGINX_WORKER_PROCESSES:-2}/g" \
-e "s/\${APP}/${APP:-undefined}/g" \
-e "s/\${APP_ENV}/${APP_ENV:-test}/g" \
/etc/nginx/nginx.conf
sed -i -e "s/pm = static/pm = ${FPM_PM:-static}/g" \
-e "s/pm.max_children = 50/pm.max_children = ${FPM_MAX_CHILDREN:-20}/g" \
-e "s/pm.start_servers = 20/pm.start_servers = ${FPM_START_SERVERS:-10}/g" \
/etc/php/8.4/fpm/pool.d/www.conf
exec /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf
运行环境构建脚本
使用以下脚本(build.sh)构建运行环境镜像:
#!/bin/bash
set -e
# 配置
REGISTRY="harbor.leops.local"
IMAGE_BASE_NAME="common/runtime/yii"
VERSION="8.4"
# 声明镜像地址数组
declare -a IMAGE_PATHS
IMAGE_PATHS+=(
"${REGISTRY}/${IMAGE_BASE_NAME}:php-${VERSION}"
"${REGISTRY}/${IMAGE_BASE_NAME}:php-${VERSION%.*}"
"${REGISTRY}/${IMAGE_BASE_NAME}:php-${VERSION}-debian11"
"${REGISTRY}/${IMAGE_BASE_NAME}:php-${VERSION%.*}-debian11"
)
build_image() {
echo "Building and pushing image:"
for img in "${IMAGE_PATHS[@]}"; do echo -n " $img"; done
# 构建镜像
docker buildx build \
$(for img in "${IMAGE_PATHS[@]}"; do echo -n "-t $img "; done) \
--label "org.opencontainers.image.created=$(date --rfc-3339=seconds)" \
--build-arg "PHP_VERSION=${VERSION}" \
--provenance=false \
--load \
--pull \
--push \
.
echo "Build complete."
}
# 参数处理
case "$1" in
"list-tags")
# 输出镜像标签列表
printf '%s\n'"${IMAGE_PATHS[@]}"
;;
*)
build_image
;;
esac
构建应用镜像
在准备好 PHP 工具镜像和框架运行环境镜像后,我们可以构建具体的应用镜像。
PHP 应用容器化的最佳实践
PHP 应用容器化需要注意以下几点:
- 使用非 root 用户:www-data 用户是标准选择,提高安全性
- 合理的目录结构:将应用代码、配置、日志等分开管理
- 环境变量注入:通过环境变量传递配置,支持不同环境部署
- 监控集成:添加健康检查和监控端点
准备示例应用
首先,我们获取一个示例 Yii 框架应用:
git clone https://github.com/lework/ci-demo-yii.git
cd ci-demo-yii
应用 Dockerfile 详解
下面是应用的 Dockerfile,它基于我们的 Yii 框架运行环境:
#syntax=harbor.leops.local/library/docker/dockerfile:1
FROM harbor.leops.local/common/runtime/yii:php-8.4 AS running
ARG APP_ENV=test \
APP=undefine
ENV APP_ENV=$APP_ENV \
YII_ENV=$APP_ENV \
APP=$APP
COPY --chown=www-data:www-data . /app/${APP}
这个简洁的 Dockerfile 做了几件关键事情:
- 使用框架运行环境:继承了我们之前构建的 Yii 运行环境
- 环境配置:通过 ARG 和 ENV 指令设置应用环境变量
- 代码部署:将应用代码复制到容器内的特定目录
- 权限设置:确保文件归属于 www-data 用户
如果说你的项目代码中没有vendor目录,而是从composer install后生成的vendor目录,那么你需要使用下面的 Dockerfile:
#syntax=harbor.leops.local/library/docker/dockerfile:1
# ---- 编译环境 ----
FROM harbor.leops.local/common/tools/php:8.4 AS builder
ARG APP_ENV=test \
APP=undefine
ENV APP_ENV=$APP_ENV \
APP=$APP
COPY composer.json composer.lock .
RUN --mount=type=cache,id=${APP}-composer,target=/app/vendor \
composer install \
--prefer-dist \
--no-dev \
--no-interaction \
--no-plugins \
--no-scripts \
--optimize-autoloader \
&& cp -rf vendor /tmp/vendor
# ---- 运行环境 ----
FROM harbor.leops.local/common/runtime/yii:php-8.4 AS running
ARG APP_ENV=test \
APP=undefine
ENV APP_ENV=$APP_ENV \
APP=$APP
COPY --from=builder --link /tmp/vendor /app/${APP}/vendor
COPY --chown=www-data:www-data . /app/${APP}
构建应用镜像
执行以下命令构建示例应用:
bash /data/dockerfiles-base/app-build/build-app.sh dev ci-demo-yii
构建完成后,会生成如下格式的镜像标签:
harbor.leops.local/dev/ci-demo-yii:master-0d298db-202504252256
版本控制
完成构建后,将配置文件提交到版本控制系统:
git add -A .
git commit -m "feat: add php yii application"
git push
运行与验证
容器运行与验证
使用以下命令运行和测试应用容器:
# 运行容器
docker run --rm -d --name ci-demo-yii -p 18081:80 harbor.leops.local/dev/ci-demo-yii:master-0d298db-202504252256
# 访问应用
curl http://localhost:8080
# 查看日志
docker logs ci-demo-yii
# 停止容器
docker stop ci-demo-yii
生产环境最佳实践
在生产环境中部署 PHP 应用容器时,可以考虑以下最佳实践:
- 资源限制:使用 --memory 和 --cpus 设置容器资源上限
- 持久化存储:对数据库文件、上传文件等使用卷挂载
- 环境变量管理:使用配置管理工具或 Kubernetes ConfigMap 管理环境变量
- 健康检查:配置 Docker 健康检查,及时发现问题
- 日志收集:将日志输出到 stdout/stderr,使用日志收集工具集中管理
- 容器编排:在生产环境中使用 Kubernetes 等工具进行编排
总结
通过采用多阶段分层设计和遵循最佳实践,我们成功为 PHP 应用程序创建了优化、安全的 Docker 镜像。这种方法具有以下优势:
- 标准化构建流程:从基础系统到工具环境、框架运行环境,再到应用镜像,形成完整链条
- 镜像复用:各层镜像可被多个应用共享,节省存储空间和构建时间
- 安全性高:使用非 root 用户运行,减少攻击面
- 易于维护:各层次清晰分离,更新某一层不影响其他层
- 性能优化:通过合理的配置,优化 Nginx 和 PHP-FPM 性能
这种 PHP 应用容器化方案不仅保持了 PHP 应用的灵活性,还充分利用了容器技术的优势,为现代 Web 应用提供了可靠的部署方案。