Docker Compose 部署 LNMP 建站实战:Nginx、MySQL、PHP 一次讲清


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 GatewayPHP 容器未启动或 fastcgi_pass 地址错误看 docker compose ps 与 PHP 日志,确认服务名是 php
File not foundNginx 的 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 来说,前期花一两个小时把目录和配置规划好,后面能省下大量重复劳动。


手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

网站备份实战:crontab 定时自动备份网站与数据库,异地存储与一键恢复一次讲清

Docker 部署实战:从安装到容器化网站与自动更新一次讲清

评 论