LNMP(Linux + Nginx + MySQL + PHP)一直是国内站长最常用的建站组合。传统装法要逐条命令安装,环境一多就容易出现版本冲突、扩展缺失、迁移困难。用 Docker Compose 把 Nginx、PHP-FPM、MySQL、Redis 编排成一组容器后,整个环境可以用一个 YAML 文件描述,换服务器时复制目录、执行一条命令就能还原,特别适合同时维护多个站点的 VPS。
这篇按实际部署顺序,从安装 Docker、规划目录、编写 compose 文件,到 Nginx 配置、HTTPS、备份和常见故障,把一套可上线的 LNMP 环境讲清楚。示例基于 Ubuntu 22.04 / Debian 12,其他发行版命令略有差异。
一、为什么用 Docker Compose 部署 LNMP
- 环境一致:PHP 版本、扩展、MySQL 配置全部写在镜像和配置文件里,本地和线上表现一致。
- 隔离干净:每个站点一套容器,互不影响,删站不留残留。
- 迁移方便:compose 文件 + 数据目录 + 站点目录,打包就能搬到新机器。
- 升级可控:改一个镜像 tag 就能升级,回滚也只是改回来,比手工编译省事。
代价是要理解容器网络、目录挂载和端口映射。下面按顺序来。
二、环境准备:安装 Docker 与 Compose
先更新系统并安装 Docker 官方仓库版本,发行版自带版本通常偏旧:
apt update && apt install -y ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/docker.list
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
国内机器拉镜像慢,可以配置镜像加速,地址按云厂商公开的替换:
mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<'EOF'
{
"registry-mirrors": ["https://docker.m.daocloud.io"],
"log-driver": "json-file",
"log-opts": { "max-size": "100m", "max-file": "3" }
}
EOF
systemctl daemon-reload && systemctl enable --now docker
验证版本:
docker version
docker compose version
注意:现在的 Compose V2 是 docker compose(中间是空格),不是旧版的 docker-compose,网上很多老教程混用,执行前先确认。
三、目录结构规划
建议把所有容器相关文件集中放在 /opt/lnmp,站点源码、配置、数据分开挂载,备份时思路清晰:
/opt/lnmp
├── docker-compose.yml
├── nginx/
│ ├── conf.d/
│ │ └── site.conf
│ └── logs/
├── php/
│ ├── Dockerfile
│ ├── php.ini
│ └── www.conf
├── mysql/
│ ├── conf.d/
│ │ └── my.cnf
│ └── init/
│ └── 01-init.sql
├── redis/
│ └── redis.conf
├── ssl/
│ └── site.com/
└── www/
└── site.com/
├── index.php
└── ...
原则只有一条:容器里不存业务数据。数据库、上传文件、日志、证书全部落到宿主机目录,容器删了也能重建。
四、编写 docker-compose.yml
下面是一份可直接使用的基础编排,包含 Nginx、PHP-FPM、MySQL 8.0 和 Redis:
services:
nginx:
image: nginx:1.27-alpine
container_name: lnmp-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./www:/var/www/html:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/logs:/var/log/nginx
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- php
restart: unless-stopped
networks:
- lnmp
php:
build:
context: ./php
dockerfile: Dockerfile
container_name: lnmp-php
volumes:
- ./www:/var/www/html
- ./php/php.ini:/usr/local/etc/php/conf.d/custom.ini:ro
- ./php/www.conf:/usr/local/etc/php-fpm.d/www.conf:ro
depends_on:
- mysql
- redis
restart: unless-stopped
networks:
- lnmp
mysql:
image: mysql:8.0
container_name: lnmp-mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: Asia/Shanghai
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf.d:/etc/mysql/conf.d:ro
- ./mysql/init:/docker-entrypoint-initdb.d:ro
restart: unless-stopped
networks:
- lnmp
redis:
image: redis:7-alpine
container_name: lnmp-redis
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
volumes:
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf:ro
- ./redis/data:/data
restart: unless-stopped
networks:
- lnmp
networks:
lnmp:
driver: bridge
同目录再建一个 .env 文件,把密码从 compose 里拆出来:
MYSQL_ROOT_PASSWORD=换成你的强密码
MYSQL_DATABASE=site_db
MYSQL_USER=site_user
MYSQL_PASSWORD=换成你的强密码
几个容易踩坑的点:
- 不要给 MySQL 映射 3306 到公网。容器之间通过服务名
mysql访问,宿主机需要调试时再临时映射到127.0.0.1:3306。 - PHP 容器默认没有 mysqli/pdo_mysql。官方镜像需要自己装扩展,见下一节。
- .env 不要提交到代码仓库,正式环境密码只保留在服务器上。
五、PHP 扩展与配置
官方 PHP 镜像比较精简,建站常用的 mysqli、pdo_mysql、gd、zip、opcache 需要安装。推荐写一个 Dockerfile,而不是每次进容器手装:
FROM php:8.3-fpm-alpine
RUN apk add --no-cache libpng-dev libjpeg-turbo-dev freetype-dev libzip-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) pdo_mysql mysqli gd zip opcache
COPY php.ini /usr/local/etc/php/conf.d/custom.ini
php.ini 建议至少调整这几项,避免上传大文件失败和脚本超时:
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
memory_limit = 256M
date.timezone = Asia/Shanghai
opcache.enable = 1
opcache.memory_consumption = 128
opcache.validate_timestamps = 1
opcache.revalidate_freq = 60
如果用的是 WordPress,建议把 www.conf 里的进程管理方式改成 pm = dynamic,并按内存设置 pm.max_children。2 核 4G 的机器一般给 8~12 个进程就够,设置过大容易把内存吃满。
六、Nginx 站点配置
新建 nginx/conf.d/site.conf,重点是把 fastcgi_pass 指向 PHP 服务名 php:9000,而不是 127.0.0.1:
server {
listen 80;
server_name site.com www.site.com;
root /var/www/html/site.com;
index index.php index.html;
access_log /var/log/nginx/site.access.log;
error_log /var/log/nginx/site.error.log;
client_max_body_size 64m;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass php:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_read_timeout 300;
}
location ~* \.(jpg|jpeg|png|gif|webp|svg|css|js|woff2)$ {
expires 30d;
access_log off;
}
location ~ /\.(env|git|svn) {
deny all;
}
}
改完配置先检测再重载,不要直接 restart:
docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reload
七、启动、验证与日常命令
在 /opt/lnmp 下执行:
docker compose up -d
docker compose ps
docker compose logs -f --tail=100 nginx php
放一个测试文件验证 PHP 是否正常解析:
<?php
phpinfo();
?>
浏览器访问 http://服务器IP/ 能看到 PHP 信息页,说明 Nginx 到 PHP-FPM 的链路是通的。再验证数据库连接,主机名要写 mysql:
docker compose exec php php -r '
$pdo = new PDO("mysql:host=mysql;dbname=site_db;charset=utf8mb4", "site_user", "你的密码");
echo $pdo->query("select version()")->fetchColumn();
'
常用运维命令:
docker compose restart php # 重启单个服务
docker compose exec mysql mysql -usite_user -p site_db # 进入数据库
docker compose exec php sh # 进入 PHP 容器
docker compose down # 停止并删除容器,数据卷保留
docker compose pull && docker compose up -d # 拉取新镜像并重建
八、绑定域名与 HTTPS
先用 A 记录把域名解析到服务器 IP,确认 80 端口可以访问。证书推荐用 acme.sh,支持自动续期,也不用把私钥交给第三方面板:
curl https://get.acme.sh | sh -s email=you@example.com
~/.acme.sh/acme.sh --issue -d site.com -d www.site.com --webroot /opt/lnmp/www/site.com
mkdir -p /opt/lnmp/ssl/site.com
~/.acme.sh/acme.sh --install-cert -d site.com \
--key-file /opt/lnmp/ssl/site.com/key.pem \
--fullchain-file /opt/lnmp/ssl/site.com/fullchain.pem \
--reloadcmd "docker compose -f /opt/lnmp/docker-compose.yml exec nginx nginx -s reload"
然后在 Nginx 里增加 443 段,并把 80 请求跳转到 443:
server {
listen 443 ssl http2;
server_name site.com www.site.com;
root /var/www/html/site.com;
index index.php index.html;
ssl_certificate /etc/nginx/ssl/site.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/site.com/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass php:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
server {
listen 80;
server_name site.com www.site.com;
return 301 https://$host$request_uri;
}
注意证书目录要挂载进容器,前面 compose 已经挂载了 ./ssl。如果路径写错,容器内找不到证书,Nginx 会直接启动失败,记得看 docker compose logs nginx。
九、备份与迁移
容器化之后备份反而更简单,分三部分:数据库、站点文件、配置。数据库每天定时导出:
mkdir -p /opt/backup
docker compose exec -T mysql mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" \
--single-transaction --routines --triggers --databases site_db \
| gzip > /opt/backup/site_db_$(date +%F).sql.gz
写进 crontab,保留最近 7 天:
0 3 * * * cd /opt/lnmp && docker compose exec -T mysql mysqldump -uroot -p'密码' --single-transaction --databases site_db | gzip > /opt/backup/site_db_$(date +\%F).sql.gz
10 3 * * * find /opt/backup -name '*.sql.gz' -mtime +7 -delete
迁移到新服务器时,把 docker-compose.yml、.env、nginx/、php/、mysql/conf.d/、www/、ssl/ 和最近一份 SQL 备份打包过去,导入数据库后执行 docker compose up -d 即可。
十、常见问题排查
| 现象 | 原因 | 处理 |
|---|---|---|
| 502 Bad Gateway | PHP 容器未启动或 fastcgi_pass 地址错误 | 看 docker compose ps 与 PHP 日志,确认服务名是 php |
| File not found | Nginx 的 root 与 PHP 容器内路径不一致 | 两个容器必须把同一份站点目录挂载到同一路径 |
| 数据库连接失败 | 主机写成了 localhost | 容器内用服务名 mysql,不要用 127.0.0.1 |
| 上传大文件失败 | php.ini 与 Nginx client_max_body_size 太小 | 同时调大两处限制并重启 |
| 磁盘占用暴涨 | 容器日志没有限制 | daemon.json 配置 log-opts 的 max-size 与 max-file |
| 修改配置不生效 | 只改了宿主机文件,没有 reload | 执行 nginx -t 后 reload,PHP 配置需重启容器 |
另外,docker compose down -v 会删除数据卷,生产环境千万别随手加 -v;权限问题优先检查宿主机目录属主与 PHP-FPM 运行用户是否匹配,常见表现是上传目录不可写、缓存文件生成失败。
总结
Docker Compose 部署 LNMP 的核心就三件事:用 compose 描述服务与网络、用目录挂载把数据留在宿主机、用服务名做容器间通信。把这份编排跑通之后,新增站点只需加一个 Nginx 配置和一份源码目录,迁移和回滚也都变成复制文件、改镜像 tag 这么简单。对需要长期维护多个站点的 VPS 来说,前期花一两个小时把目录和配置规划好,后面能省下大量重复劳动。