Docker Compose一键部署PHP环境:DNMP完整教程(Nginx+MySQL+PHP)

凌晨两点。盯着终端里滚动的报错信息,我猛地一拍桌子。
“Nginx找不到PHP-FPM的socket文件”——这已经是今晚第8个报错了。手动装LNMP环境,我改了nginx.conf、php-fpm.conf、my.cnf三个配置文件,重启了十几次服务。窗外天都快亮了,环境还是跑不起来。
更崩溃的是,隔壁工位的老王说:“我电脑上跑得好好的啊,你是不是PHP版本不对?”
好家伙,他用PHP 7.4,我装的是8.1。MySQL他是5.7,我手滑装成了8.0。难怪代码跑不起来。
说实话,这种场景你肯定也遇到过。团队里每个人的开发环境都不一样,新人入职光配环境就要半天,老员工还得一对一指导。代码在A电脑上正常,到B电脑就炸了。部署到服务器?那更是碰运气。
直到我试了Docker Compose一键部署DNMP环境,这些问题才彻底消失。10分钟搞定Nginx、MySQL、PHP、Redis全家桶,团队所有人用同一份配置文件,再也没人说”我电脑上能跑”了。
为什么选择Docker Compose部署PHP环境
传统LNMP部署的三大痛点
痛点1:手动安装配置繁琐,一步错步步错
还记得第一次装LNMP的场景吗?先apt-get install nginx,再装php-fpm,然后装MySQL。装完还得改配置文件,nginx.conf要配location规则,php.ini要开扩展,my.cnf要调参数。
三个配置文件散落在不同目录:/etc/nginx/、/etc/php/、/etc/mysql/。一个配置写错了,服务就启动不了。排查问题得翻日志,Nginx日志、PHP-FPM日志、MySQL日志,看到头晕。
我见过有人因为php-fpm.conf里的listen = 127.0.0.1:9000写成了listen = /run/php/php7.4-fpm.sock,Nginx找不到PHP,排查了三个小时。
痛点2:版本兼容性问题让人头秃
团队里总有那么几个”版本钉子户”。老项目跑在PHP 5.6上,新项目要用PHP 8.1。MySQL 5.7的GROUP BY语法到MySQL 8.0就报错,因为8.0默认启用了ONLY_FULL_GROUP_BY模式。
更可怕的是,不同成员电脑上的版本五花八门:
- 小李的Mac装的是PHP 7.4 + MySQL 5.7
- 老王的Ubuntu是PHP 8.0 + MySQL 8.0
- 新来的实习生Windows装的是PHP 8.1 + MariaDB 10.6
代码在小李电脑上跑得好好的,推到Git上,老王拉下来就报错。测试环境又是另一套版本,线上生产环境版本还不一样。这不是找死吗?
痛点3:团队协作效率低到怀疑人生
新人入职第一天,老员工得花半天时间手把手教他装环境:
- 安装Nginx(20分钟)
- 安装PHP和扩展(30分钟,还得编译)
- 安装MySQL(15分钟)
- 配置三个服务互相通信(1小时,各种踩坑)
- 导入测试数据(10分钟)
运气好的话,下午能开始写代码。运气不好?那就明天再说吧。
我之前在一家创业公司,因为环境不一致,一个简单的用户注册功能,开发环境测试通过,上线后报”数据库连接失败”。原因是开发环境MySQL用户名root没密码,生产环境有密码,配置文件忘了改。凌晨紧急回滚,老板差点骂娘。
Docker Compose的三大优势
优势1:配置即代码,一份文件全团队共享
Docker Compose把所有配置都写进一个docker-compose.yml文件:
- 用什么版本的Nginx?(nginx:1.25-alpine)
- PHP装哪些扩展?(mysqli、pdo_mysql、redis)
- MySQL配置参数是啥?(my.cnf统一管理)
- 服务之间怎么通信?(自动DNS解析)
这个文件往Git一扔,团队所有人拉下来都是同一套环境。新人入职?clone项目,docker-compose up -d一行命令,喝杯咖啡回来环境就跑起来了。
不用再问”你MySQL密码是多少?""你php.ini在哪?""你Nginx配置给我看看”。一个文件搞定所有问题。
优势2:一键启动/销毁,干净利落
启动所有服务:
docker-compose up -d停止并删除所有容器:
docker-compose down想切换PHP版本?改一行配置image: php:7.4-fpm变成image: php:8.1-fpm,重新up一下,搞定。
不会像传统方式那样,卸载旧版本PHP还得清理一堆残留文件,生怕删错东西导致系统炸掉。Docker容器之间完全隔离,删了就删了,干干净净。
优势3:环境隔离和多版本共存
想同时跑PHP 7.4和PHP 8.1?传统方式得装两套PHP,配置两个不同端口,改Nginx配置指定不同的fastcgi_pass。麻烦死了。
用Docker Compose?在配置文件里定义两个PHP服务:
php74:
image: php:7.4-fpm
php81:
image: php:8.1-fpmNginx配置里想用哪个版本,就指向哪个服务名。老项目继续用php74,新项目用php81,互不干扰。
GitHub上star最多的DNMP项目(imeepo/dnmp)有5000+star,说明这套方案已经被大量团队验证过了。不是什么新鲜玩意儿,是成熟可靠的解决方案。
DNMP架构设计详解
什么是DNMP
DNMP其实就是LNMP的Docker版本,四个字母各代表一个组件:
- D = Docker:容器化平台,把每个服务装进独立的”集装箱”
- N = Nginx:Web服务器,接收HTTP请求,转发给PHP处理
- M = MySQL:关系型数据库,存储数据(也可以换成MariaDB、PostgreSQL)
- P = PHP:PHP-FPM运行环境,执行PHP代码
除了这四个核心组件,一般还会加上:
- Redis:缓存服务,加速数据读取
- PHPMyAdmin:数据库管理工具,可视化操作MySQL
这些服务在传统LNMP里是直接装在系统上的,混在一起。DNMP把它们分别装进Docker容器,就像把不同的软件装进不同的虚拟机里,但比虚拟机轻量得多。
服务编排架构
想象一下,你要组装一套音响系统:功放、音箱、播放器。每个设备独立工作,但需要线缆连接才能协同发声。
DNMP也是这个道理。Nginx、PHP、MySQL是三个独立的容器,但需要通过Docker网络连接:
请求处理流程:
- 浏览器发送HTTP请求到
localhost:80 - Nginx容器接收请求,发现是PHP文件
- Nginx通过9000端口转发给PHP容器(通过服务名
php) - PHP容器执行代码,需要查数据库时连接MySQL容器(通过服务名
mysql) - PHP返回结果给Nginx
- Nginx把HTML返回给浏览器
通信机制:
Docker Compose会自动创建一个桥接网络(bridge network),把所有服务连进去。每个服务都有自己的服务名,Docker自动做DNS解析。
比如PHP要连MySQL,不用写127.0.0.1:3306,直接写mysql:3306就行。Docker会自动把mysql解析成MySQL容器的IP地址。超方便。
端口映射:
- Nginx容器:80端口 → 宿主机80端口(访问 localhost就能看到网站)
- MySQL容器:3306端口 → 宿主机3306端口(可以用Navicat等工具连接)
- PHPMyAdmin容器:80端口 → 宿主机8080端口(访问 localhost:8080管理数据库)
- Redis容器:6379端口 → 宿主机6379端口
卷挂载(Volume):
容器删了,数据就没了?不会的。通过卷挂载,把容器里的重要目录映射到宿主机:
./www→ Nginx和PHP容器的/var/www/html(代码目录)./mysql/data→ MySQL容器的/var/lib/mysql(数据库文件)./nginx/logs→ Nginx容器的/var/log/nginx(访问日志)
这样即使容器删了重建,数据还在宿主机上,不会丢。
目录结构设计
标准的DNMP项目目录长这样:
dnmp/
├── docker-compose.yml # 核心编排文件,定义所有服务
├── .env # 环境变量配置(密码、端口等)
├── .gitignore # Git忽略文件(.env不上传)
├── nginx/
│ ├── conf.d/
│ │ └── default.conf # 站点配置(根目录、PHP转发规则)
│ └── logs/ # 访问日志和错误日志
│ ├── access.log
│ └── error.log
├── php/
│ ├── Dockerfile # PHP镜像定制(安装扩展)
│ ├── php.ini # PHP配置(内存限制、上传大小)
│ └── php-fpm.conf # PHP-FPM配置(进程数)
├── mysql/
│ ├── data/ # 数据库文件持久化目录
│ └── my.cnf # MySQL配置(字符集、最大连接数)
└── www/ # 项目代码目录
└── index.php # 测试文件关键文件说明:
docker-compose.yml:最核心的文件,定义用哪个镜像、挂载哪些目录、映射哪些端口.env:环境变量,比如MYSQL_ROOT_PASSWORD=123456,敏感信息单独管理nginx/conf.d/default.conf:Nginx站点配置,设置网站根目录、PHP转发规则php/Dockerfile:基于官方PHP镜像,额外安装mysqli、redis等扩展mysql/data/:数据库文件存这里,容器删了重建,数据还在
这套结构看起来文件挺多,但每个文件职责清晰。不像传统LNMP配置散落各处,找起来头疼。
10分钟实战:从零搭建DNMP环境
前置准备
1. 安装Docker
- Mac/Windows:下载Docker Desktop,安装就完事了,自带docker-compose
- Linux(Ubuntu示例):
sudo apt update sudo apt install docker.io docker-compose -y sudo systemctl start docker sudo systemctl enable docker
2. 验证安装
docker --version
# 输出:Docker version 24.0.6, build xxx
docker-compose --version
# 输出:Docker Compose version v2.21.0看到版本号就说明装好了。
3. 推荐配置
- 内存:至少4GB(Docker Desktop可以在设置里调)
- 磁盘:至少20GB空闲空间(镜像会占用一些空间)
- 网络:能访问Docker Hub(如果网速慢,可以配置国内镜像源)
快速部署方案一:使用成熟开源项目(推荐)
不想折腾?直接用开源项目,10分钟搞定。
1. 克隆项目
我推荐imeepo/dnmp,支持Arm CPU(Apple M系列芯片也能用):
git clone https://github.com/imeepo/dnmp.git
cd dnmp2. 配置环境变量
复制示例配置文件:
cp .env.example .env打开.env文件,改几个关键配置:
# MySQL root密码(别用123456,太弱了)
MYSQL_ROOT_PASSWORD=your_strong_password
# 时区设置
TZ=Asia/Shanghai
# 端口映射(如果80端口被占用,改成8080)
NGINX_HTTP_PORT=80
MYSQL_PORT=33063. 一键启动
docker-compose up -d-d参数表示后台运行。第一次启动会拉取镜像,可能需要几分钟。看到这样的输出就成功了:
Creating network "dnmp_default" with the default driver
Creating dnmp_mysql_1 ... done
Creating dnmp_php_1 ... done
Creating dnmp_nginx_1 ... done
Creating dnmp_redis_1 ... done4. 验证安装
打开浏览器,访问http://localhost,应该能看到phpinfo页面,显示PHP版本、已安装扩展等信息。
看到这个页面,就说明Nginx和PHP都跑起来了。
再访问http://localhost:8080,应该能看到PHPMyAdmin登录界面:
- 服务器:
mysql(注意不是localhost) - 用户名:
root - 密码:你在.env里设置的密码
登录成功,说明MySQL也正常了。
5. 查看容器状态
docker-compose ps输出应该类似这样:
Name Command State Ports
--------------------------------------------------------------------
dnmp_nginx_1 nginx -g daemon off; Up 0.0.0.0:80->80/tcp
dnmp_php_1 php-fpm Up 9000/tcp
dnmp_mysql_1 docker-entrypoint... Up 0.0.0.0:3306->3306/tcp
dnmp_redis_1 redis-server Up 6379/tcpState显示Up就说明容器在运行。
自定义方案二:手写docker-compose.yml(进阶)
想深入理解原理?自己写配置文件。
1. 创建项目目录
mkdir my-dnmp && cd my-dnmp
mkdir -p nginx/conf.d php mysql/data www2. 编写docker-compose.yml
创建docker-compose.yml,内容如下:
version: '3.8'
services:
# Nginx服务
nginx:
image: nginx:1.25-alpine # 使用alpine版本,镜像更小
container_name: dnmp-nginx
ports:
- "80:80" # 映射80端口到宿主机
volumes:
- ./www:/var/www/html # 代码目录
- ./nginx/conf.d:/etc/nginx/conf.d # 站点配置
- ./nginx/logs:/var/log/nginx # 日志目录
depends_on:
- php # 依赖PHP服务,会先启动PHP再启动Nginx
networks:
- dnmp-network
# PHP服务
php:
build: ./php # 使用Dockerfile构建镜像
container_name: dnmp-php
volumes:
- ./www:/var/www/html # 代码目录(和Nginx保持一致)
networks:
- dnmp-network
# MySQL服务
mysql:
image: mysql:8.0
container_name: dnmp-mysql
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: root123456 # root密码
MYSQL_DATABASE: test_db # 默认创建的数据库
TZ: Asia/Shanghai # 时区
volumes:
- ./mysql/data:/var/lib/mysql # 数据持久化
networks:
- dnmp-network
# Redis服务(可选)
redis:
image: redis:7-alpine
container_name: dnmp-redis
ports:
- "6379:6379"
networks:
- dnmp-network
networks:
dnmp-network:
driver: bridge # 桥接网络,容器间可以互相访问3. 创建PHP Dockerfile
创建php/Dockerfile,安装常用扩展:
FROM php:8.1-fpm
# 安装系统依赖
RUN apt-get update && apt-get install -y \
libzip-dev \
zip \
unzip
# 安装PHP扩展
RUN docker-php-ext-install \
mysqli \
pdo_mysql \
zip \
opcache
# 安装Redis扩展
RUN pecl install redis && docker-php-ext-enable redis
# 设置工作目录
WORKDIR /var/www/html4. 创建Nginx站点配置
创建nginx/conf.d/default.conf:
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php index.html;
# 访问日志
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# PHP文件转发给PHP-FPM处理
location ~ \.php$ {
fastcgi_pass php:9000; # php是服务名,Docker自动解析IP
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# 静态文件直接返回
location ~ \.(js|css|png|jpg|gif|ico)$ {
expires 7d;
}
}5. 创建测试文件
创建www/index.php:
<?php
phpinfo();创建www/db_test.php测试数据库连接:
<?php
$host = 'mysql'; // 服务名,不是localhost
$user = 'root';
$pass = 'root123456';
$db = 'test_db';
try {
$pdo = new PDO("mysql:host=$host;dbname=$db", $user, $pass);
echo "数据库连接成功!<br>";
echo "MySQL版本: " . $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
} catch(PDOException $e) {
echo "连接失败: " . $e->getMessage();
}6. 启动服务
docker-compose up --build -d--build参数表示重新构建PHP镜像。第一次启动会比较慢,因为要下载基础镜像和安装扩展。
验证和测试
1. 测试PHP
访问http://localhost,应该能看到phpinfo页面,检查一下:
- PHP版本是8.1
- mysqli、pdo_mysql、redis扩展都显示enabled
2. 测试数据库连接
访问http://localhost/db_test.php,应该看到”数据库连接成功”和MySQL版本号。
如果报连接失败,检查:
- MySQL容器是否启动:
docker-compose ps - host是否写成了
localhost(应该是mysql) - 密码是否正确
3. 检查容器状态
docker-compose ps所有容器的State都应该是Up。
4. 查看日志排错
如果某个容器启动失败,查看日志:
docker-compose logs php # 查看PHP容器日志
docker-compose logs -f nginx # 实时查看Nginx日志(-f参数)日志会告诉你具体哪里出错了,比如配置文件语法错误、端口被占用等。
团队协作最佳实践
版本控制策略
关键原则:配置上传,敏感信息不上传
创建.gitignore文件:
# 敏感信息不上传
.env
# 数据库文件不上传(太大)
mysql/data/
# 日志文件不上传
nginx/logs/*.log
php/logs/*.log
# 代码目录由业务项目管理,不在DNMP里
www/*
!www/.gitkeep # 保留目录结构提交到Git的文件:
git add docker-compose.yml
git add .env.example # 示例配置,不含真实密码
git add nginx/conf.d/
git add php/Dockerfile
git add php/php.ini
git add mysql/my.cnf
git commit -m "feat: add DNMP environment config"
git push新成员入职流程(3分钟):
- 克隆项目:
git clone xxx - 复制配置:
cp .env.example .env - 修改密码:编辑
.env,改MYSQL_ROOT_PASSWORD - 启动环境:
docker-compose up -d - 导入数据:
docker exec -i dnmp-mysql mysql -uroot -p < backup.sql
完事。不用问老员工”你Nginx配在哪?""你PHP装了啥扩展?”全都在配置文件里。
多PHP版本共存方案
场景:老项目跑PHP 7.4,新项目要PHP 8.1,怎么办?
方案一:在docker-compose.yml定义多个PHP服务
services:
php74:
image: php:7.4-fpm
container_name: dnmp-php74
volumes:
- ./www:/var/www/html
networks:
- dnmp-network
php81:
image: php:8.1-fpm
container_name: dnmp-php81
volumes:
- ./www:/var/www/html
networks:
- dnmp-networkNginx配置不同项目指向不同PHP版本:
nginx/conf.d/old-project.conf(老项目):
server {
listen 80;
server_name old.local;
root /var/www/html/old-project;
location ~ \.php$ {
fastcgi_pass php74:9000; # 指向PHP 7.4
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}nginx/conf.d/new-project.conf(新项目):
server {
listen 80;
server_name new.local;
root /var/www/html/new-project;
location ~ \.php$ {
fastcgi_pass php81:9000; # 指向PHP 8.1
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}配置hosts文件:
127.0.0.1 old.local
127.0.0.1 new.local访问http://old.local走PHP 7.4,访问http://new.local走PHP 8.1。互不干扰。
数据持久化和备份
数据持久化已经通过卷挂载实现了,./mysql/data映射到容器的/var/lib/mysql,容器删了数据还在。
数据库备份脚本:
创建scripts/backup.sh:
#!/bin/bash
BACKUP_DIR="./backups"
DATE=$(date +%Y%m%d_%H%M%S)
MYSQL_CONTAINER="dnmp-mysql"
MYSQL_USER="root"
MYSQL_PASSWORD="root123456"
DATABASE="test_db"
mkdir -p $BACKUP_DIR
echo "开始备份数据库 $DATABASE..."
docker exec $MYSQL_CONTAINER mysqldump -u$MYSQL_USER -p$MYSQL_PASSWORD $DATABASE > $BACKUP_DIR/${DATABASE}_${DATE}.sql
echo "备份完成: $BACKUP_DIR/${DATABASE}_${DATE}.sql"定期备份(使用cron):
# 编辑crontab
crontab -e
# 每天凌晨2点备份
0 2 * * * /path/to/scripts/backup.sh恢复数据库:
docker exec -i dnmp-mysql mysql -uroot -proot123456 test_db < ./backups/test_db_20251218.sql常见问题和踩坑指南
问题1:端口被占用,容器启动失败
现象:
Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use排查:
# Mac/Linux
lsof -i :80
# Windows
netstat -ano | findstr :80解决方案:
- 方案1:停掉占用80端口的程序(比如Apache、IIS)
- 方案2:改端口映射,在
.env里改NGINX_HTTP_PORT=8080,访问localhost:8080
问题2:文件权限问题(Linux/Mac)
现象:
- Nginx报
403 Forbidden - PHP无法写入文件,报
Permission denied
原因:
容器内www-data用户(UID 33)和宿主机用户(UID 1000)不一致,导致无法访问挂载的文件。
临时方案(开发环境):
chmod -R 777 ./www正确方案(修改容器用户UID):
修改php/Dockerfile,让容器用户UID匹配宿主机:
FROM php:8.1-fpm
# 修改www-data用户的UID为宿主机用户的UID(比如1000)
RUN usermod -u 1000 www-data && groupmod -g 1000 www-data
# 其他配置...重新构建镜像:
docker-compose build php
docker-compose up -d问题3:PHP扩展缺失
现象:
Fatal error: Call to undefined function mysqli_connect()排查:
docker exec dnmp-php php -m # 查看已安装扩展如果没看到mysqli,说明扩展没装。
解决方案:
修改php/Dockerfile,添加扩展:
RUN docker-php-ext-install mysqli pdo_mysql重新构建:
docker-compose build php
docker-compose restart php问题4:数据库连接失败
现象:
SQLSTATE[HY000] [2002] Connection refused常见错误:
- 错误1:host写成了
localhost或127.0.0.1 - 错误2:MySQL容器还没启动完成就连接了
- 错误3:密码错了
正确姿势:
$host = 'mysql'; // 必须用服务名,不是localhost
$user = 'root';
$pass = 'root123456'; // 检查是否和.env里一致
try {
$pdo = new PDO("mysql:host=$host;dbname=test_db", $user, $pass);
echo "连接成功";
} catch(PDOException $e) {
echo "失败: " . $e->getMessage();
}如果还是连不上,检查MySQL是否启动完成:
docker-compose logs mysql看到mysqld: ready for connections就说明启动好了。
问题5:容器启动后立即退出
现象:
docker-compose ps
# 某个容器Status显示Exit 1或Exit 127排查命令:
docker-compose logs 服务名常见原因:
配置文件语法错误:
- Nginx配置少了分号
; - docker-compose.yml缩进不对(必须用空格,不能用Tab)
- Nginx配置少了分号
环境变量缺失:
- MySQL没设置
MYSQL_ROOT_PASSWORD,启动失败
- MySQL没设置
依赖服务未启动:
- Nginx依赖PHP,但PHP容器起不来
解决方法:
根据日志提示修改配置文件,然后重新启动:
docker-compose down # 删除所有容器
docker-compose up -d # 重新创建并启动结论
说了这么多,Docker Compose部署DNMP环境的核心价值其实就三点:
- 省时间:10分钟搞定,省下的时间可以多喝两杯咖啡
- 少踩坑:团队环境统一,再也不用半夜紧急排查”我电脑上能跑”的bug
- 易维护:配置文件就是文档,新人看一眼就懂,不用老员工手把手教
我之前手动装LNMP,经常搞到凌晨两点还跑不起来。自从用了Docker Compose,真的是”up一下,喝杯咖啡,环境就好了”。
别犹豫了,下班前花10分钟试一试。先clone个开源项目(imeepo/dnmp)跑起来,感受一下一键启动的爽感。等熟悉了,再自己写配置文件深入学习。
对了,如果遇到问题,别慌:
- 先看
docker-compose logs查日志 - 搜一下GitHub Issues,大概率别人也遇到过
- 实在不行,到项目仓库提Issue,社区很活跃的
最后说一句,Docker Compose不只能部署PHP环境,Node.js、Python、Go都可以。学会了这套思路,以后换技术栈也不怕环境配置了。
下一步行动:
- 立即行动:
git clone https://github.com/imeepo/dnmp.git,跑起第一个DNMP环境 - 深入学习:看Docker Compose官方文档,学学更高级的用法(比如docker-compose.override.yml)
- 分享传播:如果这篇文章帮到你了,分享给还在手动装环境的同事,让他们也解放一下
12 分钟阅读 · 发布于: 2025年12月18日 · 修改于: 2025年12月26日



评论
使用 GitHub 账号登录后即可评论