Docker Compose 搭建 LAMP 环境
本地手工装一套 LAMP 往往要在操作系统、PHP 版本、MySQL 配置上反复折腾,换一台机器就得重来。用 Docker Compose 把 Nginx、PHP-FPM、MySQL 三个容器编排到一起之后,整个环境用两条命令就能启动,开发机、测试服务器、生产环境跑的是同一套镜像,版本差异带来的"本地能跑、线上挂了"问题会大幅减少。对个人开发者来说还有一个额外的好处:docker-compose.yml 本身就是一份可复现的环境文档,团队新成员 clone 下来就能跑,不需要在群里接力传截图。
目录结构与配置文件
先建好项目目录,把代码放在 www/ 下,Nginx 的配置单独放一份:
lamp/
├── docker-compose.yml
├── .env
├── nginx/
│ └── default.conf
└── www/
└── index.php
docker-compose.yml 的核心内容如下。注意 MySQL 的密码、数据库名都通过环境变量注入,而不是写死在文件里:
version: '3.8'
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./www:/var/www/html
depends_on:
- php
php:
image: php:8.2-fpm
volumes:
- ./www:/var/www/html
environment:
- DB_HOST=mysql
- DB_NAME=${DB_NAME:-appdb}
- DB_USER=${DB_USER:-appuser}
- DB_PASS=${DB_PASS:-changeme}
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASS:-rootpass}
MYSQL_DATABASE: ${DB_NAME:-appdb}
MYSQL_USER: ${DB_USER:-appuser}
MYSQL_PASSWORD: ${DB_PASS:-changeme}
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db_data:
参考:Docker Compose 官方文档 https://docs.docker.com/compose/
.env 与启动命令
同目录下创建 .env,把真实密码放进去,并确保它被加入 .gitignore:
DB_NAME=mywebsite
DB_USER=webapp
DB_PASS=你的强密码
MYSQL_ROOT_PASS=另一个强密码
启动与常用操作:
docker compose up -d # 后台启动全部服务
docker compose ps # 查看容器状态
docker compose logs -f php # 跟踪 PHP 日志
docker compose down # 停止但不删数据
docker compose down -v # 停止并删除数据库卷(慎用)
Nginx 如何把请求转给 PHP-FPM
PHP 容器跑的是 php-fpm,需要通过 Nginx 的 fastcgi 转发。在 nginx/default.conf 里这样写:
server {
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$ {
fastcgi_pass php:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
这里的 fastcgi_pass php:9000 用的是 Compose 网络里的服务名 php,而不是 IP 地址,容器重启后 IP 变化也不影响。此外记得在 nginx/default.conf 里配 client_max_body_size,否则上传图片或文件超过默认 1MB 时会被 413 拒绝。
为什么选这三个镜像
选 nginx:alpine 而不是 nginx:latest,是因为 alpine 版本体积更小(约 50MB),启动更快、占用内存更少,对单机部署很友好。php:8.2-fpm 是官方维护的 PHP-FPM 镜像,配合 Nginx 的 fastcgi 转发是最常见的 PHP 部署方式;如果要装 gd、pdo_mysql 这类扩展,可以基于它再写一层 Dockerfile 封装。mysql:8.0 的数据要放在命名卷 db_data 里,这样容器删了重建数据也不会丢;千万别把数据库文件放在匿名目录,否则 docker compose down 之后可能什么都没了。如果业务要用 MariaDB,把镜像换成 mariadb:11 即可,参数基本一致。
各服务分工
| 服务 | 镜像 | 职责 |
|---|---|---|
| nginx | nginx:alpine | Web 服务器与反向代理 |
| php | php:8.2-fpm | 运行 PHP 应用 |
| mysql | mysql:8.0 | 关系型数据库 |
参考:Nginx 官方镜像 https://hub.docker.com/_/nginx ;PHP 官方镜像 https://hub.docker.com/_/php
常见问题
- PHP 连不上 MySQL。容器刚启动时 MySQL 可能还没就绪,PHP 进程会报 "connection refused"。利用上面的
healthcheck,并在 PHP 代码里做几次重试即可;也可以在 Compose 中给 php 服务加depends_on: condition: service_healthy。 - 文件权限导致 502。挂载目录的所有权不一致会让 FPM 读不到文件,确认
www/目录的属主与容器内www-data用户匹配,必要时执行chown -R www-data:www-data。 - 改了配置不生效。Nginx 与 PHP 的配置改动需要重启对应容器:
docker compose restart nginx。 - 时区与编码不一致。在
environment中加上TZ=Asia/Shanghai(或你的地区),避免日志时间和数据库时间对不上。
常见问题
启动后访问出现 502 怎么办? 先看 docker compose ps 确认三个容器都起来了,再看 docker compose logs php 有没有报错;多半是 fastcgi_pass 的服务名写错,或者 www 目录里没有 index.php。为什么浏览器里改代码要刷新好几次? 挂载的代码是实时同步的,但 PHP-FPM 对 opcode 有缓存,开发环境可以关闭 opcache,或者每次 docker compose restart php。想加 Redis 怎么办? 在 services 下加一个 redis:7 服务,PHP 代码里用 redis 作为主机名连接即可,Compose 网络是自动互通的。MySQL 容器吃内存很多怎么办? 可以在 mysql 服务里加 command: --innodb-buffer-pool-size=128M 限制缓冲池,开发机内存小的话很有效。
开发与生产的差异
开发环境图的是方便:代码挂载实时生效、日志直接跟、改配置重启容器。生产环境则相反,追求稳定和可预期。两者用同一份 compose 文件起步没问题,但上线前至少要做三处调整:一是把 image 固定到具体版本(比如 php:8.2-fpm 带上精确 tag),而不是跟着 latest 走;二是给服务加 restart: always 并配置日志轮转;三是把数据库卷和数据备份纳入日常运维计划。想在开发与生产之间切换,可以用 docker compose -f docker-compose.yml -f docker-compose.prod.yml 覆盖配置,公共部分写一份就够了。
上线前的安全检查
环境能跑起来只是第一步,生产环境的健壮性来自下面几条硬约束:
- 改掉
.env里的默认密码,生产环境务必使用强密码。 - MySQL 的 3306 端口默认不暴露到宿主机,只在内网网络里可达,保持这个设置。
- 给每个服务加
restart: always,避免服务器重启后服务起不来。 - 生产部署建议参考Docker Compose 生产部署,并配合Nginx 反向代理统一入口。
如果你刚开始接触容器化,可以先看看环境部署分类下的入门文章;需要把整套 LNMP 再梳理一遍的话,LNMP 环境搭建指南会更系统。