ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP8.5配置Docker环境部署怎么操作

PHP8.5配置Docker环境部署怎么操作 前言把 PHP 项目搬进 Docker看起来是照抄官方文档就行实际动手时最常见的三个卡点是镜像拉下来了但项目需要的扩展pdo_mysql、gd、intl一个都没有本地改完代码刷新页面没变化找了半天发现是 OPcache 把旧文件缓存住了Nginx 报502 Bad Gateway日志里写着连不上 FPM但 FPM 容器明明在跑。PHP 8.5 的正式版在 2025 年 11 月发布官方 Docker 镜像同步提供了php:8.5-cli、php:8.5-fpm、php:8.5-apache等标签。对绝大多数项目来说推荐用php:8.5-fpm配一个独立的 Nginx 容器——Nginx 处理静态文件和并发连接更合适两个容器也能各自独立扩缩容。本文给出一个可以直接docker compose up跑起来的最小生产向配置重点讲清扩展怎么装、配置怎么进容器、权限怎么不打架。一、目录结构与环境事实先说清楚要落地成什么样子。本文用的目录结构project/ ├── docker/ │ ├── php/ │ │ ├── Dockerfile │ │ └── conf.d/ │ │ ├── php.ini │ │ └── www.conf │ └── nginx/ │ └── default.conf ├── src/ │ └── index.php ├── .dockerignore └── compose.yaml动手前先确认两件环境事实。第一Docker 本身可用第二官方镜像确实有 8.5 标签docker --version docker compose version # 先确认镜像能拉到再往里加东西避免在 Dockerfile 里踩空 docker run --rm php:8.5-cli php -v docker run --rm php:8.5-cli php -m第二条命令会列出这个镜像已经编译好的扩展。官方镜像默认带了一批常用扩展动手前先看一眼能省掉一次重新编译通常已内置需要自己docker-php-ext-installcurl、mbstring、openssl、sodiumpdo_mysql、mysqlijson、ctype、fileinfo、tokenizergd需要额外系统库pdo_sqlite、sqlite3、dom、xmlintl需要 ICUfilter、hash、session、zipopcache需显式启用先php -m再决定装什么是这一节最重要的动作因为反复编译镜像会显著拉长构建时间。二、写 Dockerfile多阶段构建原项目里如果vendor/目录没提交进版本库就需要在镜像里跑 Composer。用多阶段构建把 Composer 阶段和运行时阶段分开可以让最终镜像不带 Composer 和它的缓存。# ---------- 阶段一安装依赖 ---------- FROM composer:2 AS vendor WORKDIR /app # 先只复制依赖描述文件让这两层能被 Docker 缓存住 COPY composer.json composer.lock ./ # --no-dev 跳过开发依赖--no-scripts 避免在依赖装完前执行项目脚本 RUN composer install \ --no-dev \ --no-interaction \ --no-progress \ --no-scripts \ --prefer-dist \ --optimize-autoloader # 再复制源码生成最终自动加载文件 COPY . . RUN composer dump-autoload --no-dev --optimize --classmap-authoritative # ---------- 阶段二运行时 ---------- FROM php:8.5-fpm-alpine # 时区Alpine 默认没有 tzdata RUN apk add --no-cache tzdata \ cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ echo Asia/Shanghai /etc/timezone \ apk del tzdata # 编译扩展需要一整套构建工具用虚拟包装上装完连同依赖一起删掉 RUN set -eux; \ apk add --no-cache --virtual .build-deps \ $PHPIZE_DEPS \ libpng-dev \ libjpeg-turbo-dev \ freetype-dev \ icu-dev \ oniguruma-dev; \ docker-php-ext-configure gd --with-freetype --with-jpeg; \ docker-php-ext-install -j$(nproc) \ pdo_mysql \ mysqli \ gd \ intl \ opcache; \ apk del .build-deps; \ rm -rf /var/cache/apk/* # 拷贝上一个阶段产出的依赖 WORKDIR /var/www/html COPY --fromvendor /app/vendor ./vendor COPY . . # 覆盖 php.ini 与 FPM 配置 COPY docker/php/conf.d/php.ini /usr/local/etc/php/conf.d/zz-app.ini COPY docker/php/conf.d/www.conf /usr/local/etc/php-fpm.d/zz-www.conf # 目录权限交给 www-data RUN chown -R www-data:www-data /var/www/html USER www-data EXPOSE 9000 CMD [php-fpm]这里有几个必须理解的点。$PHPIZE_DEPS是官方镜像预置的环境变量里面是一组编译扩展所需的开发工具autoconf、gcc、make 等。apk add --virtual .build-deps $PHPIZE_DEPS是官方推荐写法--virtual给这批包起了组名最后apk del .build-deps一次性删干净构建工具就不会留在最终镜像里。docker-php-ext-configure和docker-php-ext-install是两个不同的动作前者只配置编译参数后者才真正编译安装。像gd这种带可选特性的扩展必须先 configure 指定用哪个图像库否则编出来的gd不支持 JPEG/PNG。-j$(nproc)让编译并行多核机器上能明显缩短构建时间。zz-前缀是有意的。conf.d目录按文件名字母序加载官方镜像自带的配置排在前面用zz-前缀能保证你的配置最后加载、覆盖前面的值。官方镜像默认没有php.ini。镜像里提供的是模板/usr/local/etc/php/php.ini-production和php.ini-developmentphp.ini本身不存在所有配置走conf.d/下的独立文件。所以别去改php.ini直接往conf.d/里加文件就对了。docker/php/conf.d/php.ini的内容; 生产环境基础配置 expose_php Off memory_limit 256M max_execution_time 30 post_max_size 16M upload_max_filesize 12M ; 时区必须显式设置否则 date() 会发警告 date.timezone Asia/Shanghai ; 错误处理记录但不输出 display_errors Off log_errors On error_reporting E_ALL ; OPcache生产环境开启 opcache.enable 1 opcache.memory_consumption 128 opcache.max_accelerated_files 10000 ; 生产环境关掉时间戳校验性能更好但改代码必须重启容器 opcache.validate_timestamps 0 ; 生产环境不要开这个会拉长响应时间并泄露内部信息 opcache.jit disablewww.conf至少要覆盖监听地址和进程管理方式[www] ; 必须监听 0.0.0.0只监听 127.0.0.1 的话 Nginx 容器连不上 listen 0.0.0.0:9000 pm dynamic pm.max_children 20 pm.start_servers 4 pm.min_spare_servers 2 pm.max_spare_servers 6 pm.max_requests 500pm.max_children 20要结合容器内存限制来算不能随便填大。每个 FPM 子进程都会占用一份 PHP 内存受memory_limit影响20 个子进程乘以 256M 理论上限是 5G容器只给 512M 就会触发 OOM Killer。合理做法是先用docker stats观察单个进程的实际占用再反推子进程数。三、compose.yaml让 Nginx 与 FPM 对上话services: php: build: context: . dockerfile: docker/php/Dockerfile restart: unless-stopped volumes: # 只挂载源码不要挂载整个项目目录否则 vendor 会被本地文件覆盖 - ./src:/var/www/html/src:ro - ./public:/var/www/html/public:ro environment: TZ: Asia/Shanghai healthcheck: test: [CMD, php-fpm, -t] interval: 30s timeout: 5s retries: 3 networks: - app nginx: image: nginx:1.27-alpine restart: unless-stopped depends_on: php: condition: service_started ports: - 8080:80 volumes: - ./public:/var/www/html/public:ro - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro environment: TZ: Asia/Shanghai networks: - app networks: app: driver: bridgeNginx 配置docker/nginx/default.confserver { listen 80; server_name _; root /var/www/html/public; index index.php; charset utf-8; client_max_body_size 16m; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { # php 是 compose 里的服务名Docker 内置 DNS 会解析成容器 IP fastcgi_pass php:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; # 长时间任务导出、转码要调大否则 Nginx 先超时返回 504 fastcgi_read_timeout 300s; } # 不要把 .env、.git 之类的文件暴露出去 location ~ /\.(?!well-known) { deny all; } }三个关键点。fastcgi_pass php:9000用的是服务名不是 IP。Compose 会给每个服务建一条 DNS 记录服务名就是主机名。这也是为什么 FPM 必须监听0.0.0.0:9000——监听回环地址的话来自 Nginx 容器的连接会被直接拒绝表现就是502 Bad Gateway而 FPM 自己的日志里什么都看不到。fastcgi_param SCRIPT_FILENAME必须显式设置官方的fastcgi_params里并不包含这一行它只在fastcgi.conf里有。漏了它会得到File not found——这个报错特别有迷惑性看起来像文件不存在实际文件就在那里只是因为SCRIPT_FILENAME为空FPM 不知道该执行哪个脚本。两个容器挂载的源码路径必须能对上。上面 Nginx 的root是/var/www/html/public并把./public挂到同一位置SCRIPT_FILENAME拼出来才是 FPM 容器里真实存在的路径两边不一致又会回到 File not found。启动与验证# 构建并后台启动 docker compose up -d --build # 看 FPM 是否连上正常会返回 200 curl -I http://localhost:8080/ # 确认容器里跑的确实是 8.5 docker compose exec php php -v # 确认扩展都装上了 docker compose exec php php -m | grep -E pdo_mysql|gd|opcache # 实时看 FPM 和 Nginx 的日志 docker compose logs -f php nginx四、代码实战一个能自检环境的最小入口src/index.php不只是一段演示代码它顺便充当部署后自检页?php declare(strict_types1); // 最低要求PHP 8.0用到了 str_contains 和 match header(Content-Type: text/plain; charsetUTF-8); $required [pdo_mysql, gd, mbstring, opcache]; echo PHP 版本: , PHP_VERSION, PHP_EOL; echo SAPI: , PHP_SAPI, PHP_EOL; echo 时区: , date_default_timezone_get(), PHP_EOL; echo 当前时间: , date(Y-m-d H:i:s), PHP_EOL; echo str_repeat(-, 50), PHP_EOL; $missing []; foreach ($required as $ext) { $loaded extension_loaded($ext); printf(%-12s %s\n, $ext, $loaded ? 已加载 : 缺失); if (!$loaded) { $missing[] $ext; } } echo str_repeat(-, 50), PHP_EOL; // OPcache 状态0 表示未启用 $opcache function_exists(opcache_get_status) ? opcache_get_status(false) : false; if (is_array($opcache)) { printf( OPcache: %s (缓存脚本 %d 个)\n, ($opcache[opcache_enabled] ?? false) ? 开启 : 关闭, $opcache[opcache_statistics][num_cached_scripts] ?? 0 ); } else { echo OPcache: 未启用\n; } echo str_repeat(-, 50), PHP_EOL; if ($missing ! []) { http_response_code(500); echo 缺少扩展: , implode(, , $missing), PHP_EOL; exit(1); } // 用 match 把环境判断写得紧凑一点 $env getenv(APP_ENV) ?: production; echo match ($env) { dev 开发环境已打开详细报错, staging 预发环境, default 生产环境, }, PHP_EOL; echo 环境自检通过。, PHP_EOL;opcache_get_status(false)传false表示不返回脚本明细只返回汇总信息避免在页面上输出一大堆文件路径。function_exists(opcache_get_status)这个判断也是有意加的OPcache 没启用时这个函数不存在直接调用会报Call to undefined function把自检页本身变成报错页。常见坑点1. OPcache 把代码缓存住了改文件不生效❌ 错误写法opcache.validate_timestamps 0开发环境也这么配改一行代码刷新页面毫无变化然后开始怀疑 Docker 卷挂载是不是坏了。✅ 正确写法区分环境。生产用validate_timestamps 0换取性能开发环境用1并设置较小的检查间隔; 开发环境 opcache.validate_timestamps 1 opcache.revalidate_freq 0用多阶段构建不同 target或不同的conf.d文件来区分比在同一份配置里手工改来改去可靠得多。2. 挂载整个项目目录把vendor覆盖成了空的❌ 错误写法volumes: - ./:/var/www/html本地如果没跑过composer install容器里精心装好的vendor/就被一个空目录或旧目录盖掉了。✅ 正确写法只挂载需要热更新的源码目录或者用匿名卷把vendor保护起来volumes: - ./src:/var/www/html/src:ro - /var/www/html/vendor # 匿名卷保留镜像里的内容3. FPM 只监听回环地址Nginx 连不上❌ 错误写法listen 127.0.0.1:9000✅ 正确写法listen 0.0.0.0:9000症状是 Nginx 返回 502而 FPM 容器的日志干干净净——因为 FPM 根本没收到请求。排查时可以在 Nginx 容器里执行nc -zv php 9000确认端口通不通。4. 忘记SCRIPT_FILENAME得到迷惑性的 File not found❌ 错误写法只include fastcgi_params;里面没有SCRIPT_FILENAME。✅ 正确写法显式加上fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;并且确认root指向的路径在两个容器里都存在且一致。5. 用docker-php-ext-install装intl却没装 ICU 开发库❌ 错误写法RUN docker-php-ext-install intl构建时会失败在头文件缺失上。✅ 正确写法先装icu-dev装完再删RUN apk add --no-cache --virtual .build-deps icu-dev $PHPIZE_DEPS \ docker-php-ext-install -j$(nproc) intl \ apk del .build-deps6. 容器里以 root 运行生成的文件宿主机改不动❌ 错误写法Dockerfile 里不写USER容器默认以 root 跑生成的上传文件属主是 root宿主机上的开发者没有权限。✅ 正确写法Dockerfile 里USER www-data必须写宿主机挂载目录时用user: 1000:1000指定 UID/GID 与宿主机对应。注意 Alpine 里www-data的 UID 是 82、Debian 里是 33混用镜像时不要假设同一个数字。7. 生产镜像里留了 Composer 和编译器❌ 错误写法单阶段 Dockerfilecomposer和gcc/make全都留在最终镜像里体积膨胀、攻击面变大。另外配置文件挂载一律要加:ro忘了加只读标记同样是不必要的风险。✅ 正确写法用多阶段构建运行时阶段只从vendor阶段COPY --fromvendor /app/vendor ./vendor再写一个.dockerignore排除.git、node_modules、tests、*.log、.env既让构建缓存更稳定也避免密钥被误打进镜像层。总结环节关键做法常见错误换基础镜像先docker run --rm php:8.5-cli php -m看已有扩展上来就编译一堆已有的扩展装扩展--virtual .build-deps装完即删构建工具留在最终镜像里配置注入往conf.d/放zz-前缀文件试图修改不存在的php.ini容器互联FPM 监听0.0.0.0:9000Nginx 用服务名连监听127.0.0.1导致 502路径一致两个容器挂载位置与root对齐漏SCRIPT_FILENAME导致 File not found缓存生产关validate_timestamps开发打开开发环境被 OPcache 缓存住权限USER www-data配置挂载加:roroot 生成文件宿主机改不动在 PHP 8.5 上配置 Docker 部署真正需要理解的核心只有两条容器的文件系统是隔离的所以任何路径都要问清是在哪个容器里配置是分层的所以覆盖要靠文件名顺序保证。把这两条想清楚502、File not found、改代码不生效这几类问题基本都能在几分钟内定位。至于 8.5 本身的语法新特性跟 Docker 部署没有直接关系——先让环境跑通再谈用不用新语法。
返回列表