实践篇:11-构建 PHP 应用程序镜像

发布于:2025-07-05 ⋅ 阅读:(20) ⋅ 点赞:(0)

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 有以下几个重要特点:

  1. 基于 Debian:使用了我们之前创建的标准化 Debian 基础镜像
  2. PPA 源配置:添加了 PHP 和 Microsoft 的软件源,获取最新版本的 PHP 和 MSSQL 驱动
  3. 丰富的扩展支持:安装了常用的 PHP 扩展(mysql、redis、curl、gd 等)
  4. Composer 集成:安装并配置了 PHP 的包管理工具 Composer
  5. 权限管理:为 PHP-FPM 创建必要的目录并设置正确的权限
  6. 镜像仓库代理:配置 Composer 使用内部镜像仓库,加速依赖下载
  7. 缓存清理:每个步骤后清理不必要的缓存文件,减小镜像体积

其中 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 做了几件关键事情:

  1. 使用框架运行环境:继承了我们之前构建的 Yii 运行环境
  2. 环境配置:通过 ARG 和 ENV 指令设置应用环境变量
  3. 代码部署:将应用代码复制到容器内的特定目录
  4. 权限设置:确保文件归属于 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 应用容器时,可以考虑以下最佳实践:

  1. 资源限制:使用 --memory 和 --cpus 设置容器资源上限
  2. 持久化存储:对数据库文件、上传文件等使用卷挂载
  3. 环境变量管理:使用配置管理工具或 Kubernetes ConfigMap 管理环境变量
  4. 健康检查:配置 Docker 健康检查,及时发现问题
  5. 日志收集:将日志输出到 stdout/stderr,使用日志收集工具集中管理
  6. 容器编排:在生产环境中使用 Kubernetes 等工具进行编排

总结

通过采用多阶段分层设计和遵循最佳实践,我们成功为 PHP 应用程序创建了优化、安全的 Docker 镜像。这种方法具有以下优势:

  1. 标准化构建流程:从基础系统到工具环境、框架运行环境,再到应用镜像,形成完整链条
  2. 镜像复用:各层镜像可被多个应用共享,节省存储空间和构建时间
  3. 安全性高:使用非 root 用户运行,减少攻击面
  4. 易于维护:各层次清晰分离,更新某一层不影响其他层
  5. 性能优化:通过合理的配置,优化 Nginx 和 PHP-FPM 性能

这种 PHP 应用容器化方案不仅保持了 PHP 应用的灵活性,还充分利用了容器技术的优势,为现代 Web 应用提供了可靠的部署方案。