本地开发PHP项目时,最常见的痛点就是环境不一致:换一台电脑要重装扩展,PHP版本升级导致旧项目跑不起来,或者团队里每个人用的数据库版本都不一样。Docker把应用及其依赖打包成镜像,配合Compose编排多个服务,可以做到一条命令启动完整开发环境,真正实现“一次构建,到处运行”。下面我会从PHP镜像定制开始,一步步带你搭好一个结构清晰、易于维护的Docker开发环境。

构建适合开发场景的PHP镜像
Docker官方提供了多种PHP基础镜像,比如php:8.2-fpm、php:8.2-apache。对于大多数现代PHP应用,使用FPM配合Nginx是主流方案,因为Nginx处理静态文件更高效,而且配置文件比Apache更直观。如果你只是跑一个简单的API或者CLI脚本,也可以直接用php:8.2-cli,但开发环境中通常需要Web服务器来响应HTTP请求。
编写Dockerfile时,关键点在于用docker-php-ext-install命令安装扩展。这个脚本是官方镜像自带的,能自动完成编译和配置。例如安装PDO MySQL扩展和GD库:
FROM php:8.2-fpm
# 安装系统依赖(libpng-dev 用于 GD 库,libzip-dev 用于 zip 扩展)
RUN apt-get update && apt-get install -y \
libpng-dev \
libzip-dev \
zip \
unzip \
&& docker-php-ext-install pdo_mysql gd zip
# 安装 Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# 设置工作目录
WORKDIR /var/www/html
# 暴露 FPM 端口
EXPOSE 9000
CMD ["php-fpm"]
上面的Dockerfile做了三件事:安装操作系统级别的依赖库,启用PHP扩展,并把Composer复制进镜像。注意docker-php-ext-install只能安装官方源码中包含的扩展,像Redis、Xdebug这类PECL扩展需要额外使用pecl install。开发阶段我们通常把Xdebug放进镜像,方便调试,所以可以加上:
RUN pecl install xdebug \
&& docker-php-ext-enable xdebug
为了加快构建速度,可以把不经常变动的步骤放在前面,把经常修改的配置文件复制放在后面,充分利用Docker的层缓存。另外,开发阶段不建议为了减小镜像体积而做太多优化,因为可维护性和构建速度更重要,等到了生产环境再用多阶段构建做瘦身。
用Docker Compose编排多服务
PHP应用很少只靠一个PHP容器就能跑起来,通常还需要Nginx作为Web服务器、MySQL作为数据库、Redis作为缓存或队列驱动。手动管理多个容器的启动参数和网络会非常繁琐,Docker Compose通过YAML文件定义所有服务的关系,一条命令即可启动整个栈。下面是一个典型的docker-compose.yml示例:
version: '3.8'
services:
php:
build:
context: ./docker/php
dockerfile: Dockerfile
volumes:
- ./src:/var/www/html
- ./docker/php/php.ini:/usr/local/etc/php/conf.d/custom.ini
depends_on:
- mysql
- redis
networks:
- internal
nginx:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./src:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
networks:
- internal
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: myapp
MYSQL_USER: user
MYSQL_PASSWORD: password
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
networks:
- internal
redis:
image: redis:alpine
ports:
- "6379:6379"
networks:
- internal
volumes:
mysql_data:
networks:
internal:
driver: bridge
这个配置里,php服务使用本地构建的镜像,并把宿主机当前目录下的src文件夹挂载到容器内的/var/www/html,这样修改代码后不需要重建镜像就能立即生效。Nginx同样挂载了代码目录,同时挂载自定义配置文件,该文件需要把请求转发给php服务的9000端口。数据库数据持久化到命名卷mysql_data,即使容器删除数据也不会丢失。
关于Nginx配置,需要将PHP请求代理到FPM。一个简单的default.conf如下:
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass php:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
}
}
这里fastcgi_pass指向的是php服务名,因为Compose会在内部网络中自动创建DNS条目,Nginx容器可以通过服务名解析到PHP容器的IP。如果你使用的是Laravel或Symfony这类框架,需要把root指向public目录,并且确保try_files正确重写到入口文件。
集成Xdebug与优化开发体验
断点调试是PHP开发中不可或缺的一环,Docker环境下配置Xdebug需要特别注意网络和路径映射。首先在PHP镜像安装Xdebug,然后在php.ini中添加配置。开发环境推荐使用develop,debug模式,并开启远程调试。下面是一个适合本地开发的Xdebug配置片段:
xdebug.mode=debug xdebug.client_host=host.docker.internal xdebug.client_port=9003 xdebug.idekey=PHPSTORM xdebug.start_with_request=yes
host.docker.internal是Docker为容器提供的特殊DNS名,指向宿主机,这样IDE(例如PhpStorm或VS Code)监听9003端口就能收到来自容器的调试连接。如果你在Linux系统上,host.docker.internal可能默认不可用,需要在Compose文件中给PHP服务添加extra_hosts: - "host.docker.internal:host-gateway"。另外,路径映射必须配置正确,否则断点无法命中。IDE中把本地项目根目录映射到容器内的/var/www/html即可。
代码同步方面,我们使用卷挂载把宿主机代码目录挂载进容器,这样修改文件立即反映在容器内。但在Mac和Windows上,默认的挂载性能较差,尤其是大型项目包含成千上万文件时,访问速度会明显下降。可以通过在挂载选项中加入:delegated或:cached来提升性能。比如:
volumes: - ./src:/var/www/html:delegated
这个参数告诉Docker允许宿主机和容器之间有一定延迟同步,牺牲一点实时性换取更快的读写速度。此外,还可以把vendor目录单独挂载为匿名卷,避免宿主机和容器之间的频繁文件同步:
volumes: - ./src:/var/www/html:delegated - /var/www/html/vendor
这样容器内的vendor目录就不会被宿主机目录覆盖,Composer安装的依赖保留在容器层中,避免了Windows和Linux之间的文件权限不兼容问题。
镜像优化与生产环境建议
开发环境为了方便调试,镜像里可能包含Xdebug、Composer以及大量开发依赖,这些在生产环境中不仅增大镜像体积,还可能带来安全隐患。多阶段构建是解决这个问题的利器。你可以在第一阶段安装所有依赖并构建项目,第二阶段只复制运行必需的文件,得到一个精简的生产镜像。下面是一个简单的多阶段Dockerfile示例:
# 构建阶段
FROM composer:latest AS composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction
FROM php:8.2-fpm
RUN apt-get update && apt-get install -y \
libpng-dev \
libzip-dev \
&& docker-php-ext-install pdo_mysql gd zip
COPY --from=composer /app/vendor /var/www/html/vendor
COPY . /var/www/html
RUN chown -R www-data:www-data /var/www/html
USER www-data
EXPOSE 9000
CMD ["php-fpm"]
第二阶段从官方PHP镜像开始,只复制了vendor目录和应用代码,然后切换到非root用户www-data运行,降低容器逃逸风险。生产环境还应该移除不用的系统工具、添加健康检查、设置合理的资源限制,这些都是Docker最佳实践。
另一个常见问题是文件权限。PHP容器默认以www-data用户运行,而宿主机挂载的代码文件可能属于当前用户,导致容器内无法写入日志或缓存。解决办法是在Dockerfile里使用usermod把www-data的UID改成与宿主机用户一致,或者在启动脚本里动态修改权限。对于开发环境,更简单的做法是在Compose中设置user: "${UID}:${GID}",从环境变量传入当前用户的ID,让容器进程和宿主机用户有相同权限。这样直接在IDE里修改文件就不会遇到权限问题了。
通过以上几个部分的配置,你应该已经能够用Docker搭建一个稳定、高效的PHP开发环境。容器化开发初期可能有一些学习成本,但一旦熟悉之后,环境一致性、可移植性和团队协作效率的提升会非常明显。后续还可以进一步集成CI/CD、使用Kubernetes管理容器编排,但那是另一个话题了。