Docker配置PHP开发环境教程

发布时间:2026/10/8 23:42:32
Docker配置PHP开发环境教程 前言用 Docker 搭 PHP 环境最大的价值不是省安装步骤而是把环境本身变成可以提交到版本库的代码。换一台机器docker compose up -d之后环境完全一致不会出现我这台能跑你那台报错。但刚上手时有三类误解特别常见。第一以为拉一个php镜像就能当 Web 服务器用——实际官方php:fpm镜像里只有 PHP 解释器和 php-fpm 进程没有任何 Web 服务器浏览器访问不了php:apache镜像才自带 Apache。第二以为在容器里docker compose exec进去改php.ini就改好了——容器是临时的重建之后改动全丢任何配置都必须写进 Dockerfile 或挂载文件。第三以为 compose 里写了depends_on就等于数据库已经就绪——depends_on只保证启动顺序不保证服务可用PHP 照样可能在 MySQL 初始化完成前就发起连接。本文搭建一套 nginx php-fpm MySQL 的三容器开发环境重点讲清楚镜像选择、配置落盘、容器间寻址这三件事。命令与配置片段是静态推演的未在真实机器上执行验证镜像标签、可用版本请以 Docker Hub 上官方镜像页面为准。一、镜像怎么选官方 PHP 镜像按用途分成几个变体选错会白折腾很久镜像标签内容适用场景php:8.3-cli只有 PHP 命令行跑脚本、跑测试不提供网页服务php:8.3-fpmPHP php-fpm 进程管理器搭配独立的 nginx 容器最常见的生产形态php:8.3-apachePHP Apachemod_php线程安全单容器快速起步习惯.htaccess的项目php:8.3-fpm-alpine基于 Alpine Linux 的 fpm追求更小的镜像体积两个决策点fpm 还是 apache。追求贴近生产就用fpm加一个独立nginx容器只想少一个容器、快速看到页面用apache变体最省事。本文按前者搭。Debian 还是 Alpine。Alpine 用 musl libc镜像是小一些但编译扩展时依赖的软件包名和 Debian 不同遇到问题时网上能搜到的解法大多是 Debian 版。新手建议先用默认的 Debian 变体。标签建议固定到具体次版本如php:8.3.10-fpm而不是只用php:8.3-fpm。后者会随官方推送补丁而漂移昨天还好今天构建失败通常就是这么来的。二、用 compose 组织三个容器推荐的目录结构project/├─ docker-compose.yml├─ .env├─ docker/│ ├─ php/Dockerfile│ └─ nginx/default.conf└─ src/└─ index.phpdocker-compose.ymlservices:php:build:context: .dockerfile: docker/php/Dockerfilevolumes:- ./src:/var/www/htmlworking_dir: /var/www/htmldepends_on:mysql:condition: service_healthynginx:image: nginx:1.27-alpineports:- 8080:80volumes:- ./src:/var/www/html- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:rodepends_on:- phpmysql:image: mysql:8.0environment:MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}MYSQL_DATABASE: appMYSQL_USER: appMYSQL_PASSWORD: ${MYSQL_PASSWORD}volumes:- mysql_data:/var/lib/mysqlhealthcheck:test: [CMD, mysqladmin, ping, -h, localhost]interval: 5stimeout: 3sretries: 10volumes:mysql_data:.env与 compose 同目录不要提交到版本库MYSQL_ROOT_PASSWORDchange-me-rootMYSQL_PASSWORDchange-me-app几个要点不要写version:字段。Compose V2 规范里它已经废弃写了会打印一条警告没有实际作用。condition: service_healthy必须配合被依赖服务的healthcheck才有意义它才是等数据库真的能接受连接。只用depends_on: [mysql]属于只等启动不等就绪。数据卷mysql_data是命名卷放在文件系统里由 Docker 管理。不声明它的话MySQL 数据写在容器可写层docker compose down之后就没了。PHP 容器不需要ports。它只被 nginx 通过内部网络访问对外暴露没有意义反而扩大攻击面。只有 nginx 映射了宿主机端口示例用 8080避免和本机已有服务抢 80。docker/php/DockerfileFROM php:8.3-fpmRUN apt-get update \ apt-get install -y --no-install-recommends \libzip-dev libpng-dev libjpeg-dev libfreetype6-dev libonig-dev \ rm -rf /var/lib/apt/lists/*# gd 的 configure 参数PHP 7.4 起用 --with-freetype / --with-jpeg# 更早的版本用的是 --with-freetype-dir / --with-jpeg-dirRUN docker-php-ext-configure gd --with-freetype --with-jpeg \ docker-php-ext-install -j$(nproc) pdo_mysql mysqli gd zip# OPcache 在官方镜像里已编译好直接启用即可RUN docker-php-ext-enable opcache官方镜像提供了几个打包好的辅助脚本理解它们能省下大量时间docker-php-ext-install 扩展名...把 PHP 源码里的内置扩展编译安装会调用phpize、configure、make。部分扩展需要先装系统依赖比如zip需要libzip-dev。docker-php-ext-configure 扩展名 选项...在执行configure之前追加选项gd的 freetype/jpeg 开关就靠它。docker-php-ext-enable 扩展名启用已经编译好或镜像自带的扩展本质是往conf.d写一个 ini 文件。需要 PECL 上的第三方扩展时用pecl install 名字再docker-php-ext-enable 名字有些扩展还要先docker-php-source extract取出源码目录装完再docker-php-source delete清理。docker/nginx/default.confserver {listen 80;server_name localhost;root /var/www/html;index index.php index.html;location / {try_files $uri $uri/ /index.php?$query_string;}location ~ \.php$ {# 用 compose 里的服务名寻址不要写 127.0.0.1fastcgi_pass php:9000;fastcgi_index index.php;include fastcgi_params;fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;}}fastcgi_pass php:9000是这套配置里最关键的一行。compose 会为每个服务在自定义网络里注册 DNS 名称php 就是 PHP 容器的地址。写成127.0.0.1:9000是最常见的错误——那指向的是 nginx 容器自己结果必然是 502。三、启动、验证与进入容器# 构建并后台启动首次要编译扩展会慢一些docker compose up -d --build# 看服务状态确认没有反复重启docker compose ps# 在 php 容器里执行命令确认版本与扩展docker compose exec php php -vdocker compose exec php php -mdocker compose exec php php --ini# 实时看日志排查 502 时最有用docker compose logs -f phpdocker compose logs -f nginx# 收工时停止并保留数据卷加 -v 才会连数据一起删docker compose down注意命令写法现在是docker compose空格V2 插件早期是docker-compose连字符Python 版 V1。V1 早已停止维护新环境直接用 V2 写法。写一个src/index.php验证链路是否打通?php// 适用于 PHP 8.0declare(strict_types1);header(Content-Type: text/plain; charsetutf-8);echo PHP 版本: , PHP_VERSION, PHP_EOL;echo SAPI: , PHP_SAPI, PHP_EOL;foreach ([pdo_mysql, mysqli, gd, Zend OPcache] as $ext) {printf(%-12s %s\n, $ext, extension_loaded($ext) ? 已加载 : 缺失);}浏览器访问http://localhost:8080/SAPI应该显示fpm-fcgi。如果拿到 502按这个顺序查docker compose ps看 php 容器是不是起来了、docker compose logs php看 fpm 有没有报错、最后确认fastcgi_pass写的是服务名而不是127.0.0.1。四、容器之间怎么连数据库在容器里localhost永远是容器自己不是宿主机也不是别的容器。所以 PHP 连 MySQL 的 host 必须写服务名?php// 适用于 PHP 8.0// host 用 compose 服务名 mysql端口是容器网络内的 3306$dsn mysql:hostmysql;port3306;dbnameapp;charsetutf8mb4;$options [PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC,PDO::ATTR_EMULATE_PREPARES false, // 用服务端真预处理];try {$pdo new PDO($dsn, app, getenv(MYSQL_PASSWORD) ?: , $options);} catch (PDOException $e) {// 生产环境不要把异常原文直接输出给访问者error_log(数据库连接失败: . $e-getMessage());http_response_code(500);exit(服务暂不可用);}$stmt $pdo-prepare(SELECT VERSION() AS v, DATABASE() AS d);$stmt-execute();$row $stmt-fetch();echo MySQL 版本: , $row[v], PHP_EOL;echo 当前库: , $row[d], PHP_EOL;即使配了 healthcheck第一次up的时候仍可能赶上 MySQL 还在初始化。开发环境里给连接加一点重试是务实的做法?php// 适用于 PHP 8.0简单的带重试连接function connectWithRetry(string $dsn, string $user, string $pass, int $tries 10): PDO{$last null;for ($i 1; $i $tries; $i) {try {return new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION,]);} catch (PDOException $e) {$last $e;sleep(2);}}throw new RuntimeException(数据库始终不可用: . $last?-getMessage(), 0, $last);}自动加载 PHP 8 的空安全操作符?-需要 PHP 8.0 及以上在 PHP 7 上要写成$last null ? : $last-getMessage()。常见坑点❌ 拉php:8.3-fpm镜像映射 80 端口后用浏览器访问得到连接被重置✅fpm镜像里没有 Web 服务器只监听 FastCGI 端口。要么改用php:8.3-apache要么按本文加一个 nginx 容器。❌ 用docker compose exec php进去改/usr/local/etc/php/conf.d/xxx.ini重启容器后配置没了✅ 容器重建即丢失。改配置要么写进 Dockerfile 的RUN指令要么把宿主机文件挂载进conf.d。❌ nginx 里写fastcgi_pass 127.0.0.1:9000;页面 502✅ 容器内127.0.0.1是容器自身。要用 compose 服务名如fastcgi_pass php:9000;。❌ PHP 里写new PDO(mysql:hostlocalhost;...)报连接失败✅ 同理localhost是 PHP 容器自己。host 必须写成数据库服务的名字mysql。❌ 写了depends_on: [mysql]就认为数据库一定就绪结果偶发连接被拒绝✅depends_on只管启动顺序。要等就绪得给 MySQL 加healthcheck并用condition: service_healthy。❌ 不声明命名卷docker compose down之后数据全丢✅ MySQL 的数据目录必须挂到命名卷或宿主机目录上否则它只活在容器的可写层里。❌ 把数据库密码直接写在docker-compose.yml里提交进 Git✅ 用.env文件加${变量名}引用并把.env加入.gitignore生产环境的凭据应由编排平台的密钥机制注入。❌ 在 Dockerfile 里COPY . .且没有.dockerignore把vendor/、.git/全塞进构建上下文✅ 加.dockerignore排除vendor、.git、node_modules构建会明显更快也能避免把本地依赖覆盖进镜像。总结环节正确做法典型错误镜像选择fpm nginx 双容器或 apache 单容器以为 fpm 镜像自带 Web 服务器配置持久化写进 Dockerfile 或用挂载进容器手改 php.ini服务寻址用 compose 服务名php、mysql用 127.0.0.1 或 localhost启动顺序healthcheck 加condition: service_healthy只写depends_on数据持久化命名卷或宿主机目录依赖容器可写层凭据管理.env加.gitignore明文写在 compose 文件里把这套环境跑通的关键就三句话PHP 容器只提供 FastCGIWeb 服务器另起一个容器容器之间用服务名互相找localhost只指自己凡是希望留存的东西配置、数据、依赖都必须落在镜像构建指令或挂载卷上而不是容器里。抓住这三条Docker 配 PHP 环境就不再是靠运气的事了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询