环境不一致大概是PHP开发者最头疼的问题之一:本地用的是PHP 8.2,服务器上是7.4, composer install之后一堆扩展报错;又或者Nginx配置改来改去,重启服务直接把机器搞挂。把PHP框架项目放进Docker容器运行,可以把PHP版本、扩展、Nginx配置、依赖服务全部固化下来,一次构建,随处部署。下面从基础镜像选择、Dockerfile编写、compose编排到框架适配,完整走一遍部署流程。

一、选择基础镜像并编写Dockerfile
部署PHP框架的第一步是确定基础镜像。官方镜像分php:8.2-fpm和php:8.2-apache两种主要形态。生产环境一般推荐php-fpm配合Nginx的组合,fpm进程管理更灵活,也方便和静态资源服务解耦。如果只是快速验证,apache版本更省事,一个容器就能跑起来。
以Laravel项目为例,一个典型的Dockerfile如下:
FROM php:8.2-fpm
# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 安装系统依赖和常用PHP扩展
RUN apt-get update && apt-get install -y \
libpng-dev libjpeg-dev libfreetype6-dev libzip-dev zip unzip git \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install gd pdo_mysql mysqli bcmath zip opcache
# 安装composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY . .
RUN composer install --no-dev --optimize-autoloader --no-interaction
EXPOSE 9000
CMD ["php-fpm"]
这里有几个细节值得注意。docker-php-ext-install是官方镜像自带的扩展安装脚本,比手动编译省事得多;redis、swoole这类在PECL上的扩展要用install-php-extensions工具或者pecl install来装。另外composer建议通过多阶段构建的COPY --from方式获取,避免在镜像里下载安装脚本带来的安全隐患。
镜像体积也是绕不开的话题。php:8.2-fpm基于Debian,加上GD、zip等扩展后通常有400MB左右。如果对体积敏感,可以换用php:8.2-fpm-alpine,体积能压到100MB以内,代价是部分扩展编译时需要额外装alpine的依赖包,排错成本略高。
二、用docker-compose编排完整服务
PHP框架很少单独运行,通常离不开Nginx、MySQL和Redis。手动docker run启动多个容器并配置网络非常繁琐,用docker-compose可以把整套服务定义在一个文件里统一管理:
version: "3.8"
services:
app:
build: .
container_name: laravel-app
volumes:
- ./:/var/www/html
depends_on:
- mysql
- redis
networks:
- backend
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
volumes:
- ./:/var/www/html
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
networks:
- backend
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123456
MYSQL_DATABASE: laravel
volumes:
- db-data:/var/lib/mysql
networks:
- backend
redis:
image: redis:7-alpine
networks:
- backend
volumes:
db-data:
networks:
backend:
Nginx这边需要写一份指向fpm的配置,关键在于fastcgi_pass要指向compose里的服务名app:9000,而不是127.0.0.1。容器网络里,服务名会自动解析为对应容器IP,这是新手最容易踩的坑——很多人在Nginx配置里写localhost,结果502半天查不出原因。
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}
关于数据卷,开发阶段把代码目录挂载进容器可以实时生效,但要注意挂载会覆盖镜像里composer install产生的vendor目录,因此开发环境需要在容器内额外执行一次composer install。生产环境则建议不挂载代码,直接打包进镜像,保证运行产物和构建产物一致。
三、框架侧的配置调整与常见问题
容器化之后,框架配置要跟着改。最核心的一点是数据库和Redis的连接地址不能再写localhost,必须改成compose文件里定义的服务名,例如Laravel的.env中:
DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=laravel DB_USERNAME=root DB_PASSWORD=root123456 REDIS_HOST=redis REDIS_PORT=6379
ThinkPHP项目的调整方式类似,修改config/database.php里的hostname为mysql即可。另外还要注意存储目录权限问题:Laravel的storage和bootstrap/cache目录需要对fpm进程可写,启动后进容器执行chmod -R 775 storage bootstrap/cache,或者在Dockerfile里提前处理,否则会报无法写入日志的错。
启动顺序也值得留意。depends_on只能保证容器启动顺序,不能保证MySQL已经完成初始化。如果应用容器启动时立刻连库,可能遇到连接拒绝。简单做法是在入口脚本里加等待逻辑,或者使用带健康检查的写法:
mysql:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-proot123456"]
interval: 5s
timeout: 3s
retries: 10
配合depends_on的condition: service_healthy,就能确保数据库就绪后再启动应用。最后补充几个实用技巧:国内拉取镜像慢可以配置镜像加速器;构建时把.env、vendor、.git写进.dockerignore能显著提升构建速度;队列、定时任务可以复用同一个应用镜像,通过command: php artisan queue:work覆盖启动命令即可,无需单独构建镜像。
整套流程跑通之后,部署就简化成三条命令:docker compose build构建镜像、docker compose up -d启动服务、docker compose logs -f app查看日志。后续升级PHP版本只需改Dockerfile里的FROM行重新构建,服务器环境从此不再和项目环境打架。
PHP框架Docker部署Docker容器化PHP开发环境修改时间:2026-09-10 21:18:45