1. 项目概述与核心价值
在服务器运维和Web开发领域,环境搭建一直是个既基础又磨人的活儿。我记得刚入行那会儿,为了在CentOS上配一个能跑的LNMP(Linux, Nginx, MySQL, PHP),得照着教程一行行敲命令,处理各种依赖冲突、版本不匹配,一个下午可能就搭进去了,最后还可能因为某个配置文件里的一个分号没写对而前功尽弃。后来,Docker的出现彻底改变了这个局面。它把应用和其运行环境打包成一个轻量级、可移植的“容器”,让“一次构建,处处运行”成为现实。
今天要聊的,就是在Linux系统下,如何用Docker来搭建一个LNMP环境,并部署一个PHP项目。这不仅仅是把几个服务跑起来那么简单,而是一套现代化的、可复现的、易于管理的开发/部署工作流。对于个人开发者,这意味着你可以在几分钟内复现一个和生产环境几乎一致的开发环境;对于团队,这意味着新成员入职不再需要痛苦的环境配置,直接拉取镜像就能开工;对于运维,这意味着环境的一致性得到了保障,再也不会出现“在我机器上是好的”这种经典问题。
整个流程的核心思路是“容器化”和“编排”。我们不再直接在宿主机上安装Nginx、MySQL、PHP,而是为每个服务创建独立的Docker容器,然后通过Docker Compose这个工具,用一个配置文件把它们组织、连接起来,实现一键启动。这样做的好处是隔离性好、资源占用少、迁移和扩展极其方便。接下来,我会从环境准备开始,一步步带你完成从零到一的搭建,并分享我在这个过程中踩过的坑和积累的技巧。
2. 环境准备与Docker基础
2.1 宿主机Linux系统选择与配置
虽然Docker支持多种Linux发行版,但为了获得最好的兼容性和社区支持,我强烈推荐使用Ubuntu Server LTS(如22.04或24.04)或CentOS/Rocky Linux/AlmaLinux的较新版本。我个人更偏爱Ubuntu,因为其软件源更新快,遇到问题网上解决方案也多。这次演示就以Ubuntu 22.04 LTS为例。
在开始安装Docker之前,有几项准备工作要做。首先,确保你的系统是最新的:
sudo apt update && sudo apt upgrade -y这个命令会更新软件包列表并升级所有可升级的软件包。-y参数表示自动确认,避免中途需要手动输入。
其次,虽然不是必须,但我建议安装一些常用的工具,方便后续操作和问题排查:
sudo apt install -y curl wget vim net-toolscurl/wget: 用于从网络下载文件。vim: 一个强大的文本编辑器,后续修改配置文件会用到。net-tools: 包含netstat等网络诊断工具。
注意:如果你使用的是云服务器(如阿里云ECS、腾讯云CVM),请务必在安全组(防火墙)规则中,提前放行你需要用到的端口,例如80(HTTP)、443(HTTPS)、3306(MySQL)等。否则,即使容器内的服务运行正常,外部也无法访问。
2.2 Docker引擎的安装与验证
Docker的安装方式有很多,最稳妥的是使用Docker官方提供的安装脚本或添加其APT源。这里我们采用官方推荐的方法。
卸载旧版本(如果是全新系统可跳过):
sudo apt remove docker docker-engine docker.io containerd runc安装依赖包和GPG密钥: Docker需要一些基础依赖,并且我们需要添加Docker官方的软件源密钥,以确保下载的软件包是可信的。
sudo apt install -y ca-certificates curl gnupg lsb-release sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg这里通过管道(
|)将curl下载的密钥流直接传递给gpg --dearmor命令进行解码,并保存到指定位置。添加Docker的APT软件源:
echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null这个命令根据当前系统的架构和版本号,动态生成正确的软件源地址。
安装Docker引擎:
sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugindocker-ce是社区版引擎,docker-ce-cli是命令行工具,containerd.io是底层的容器运行时,docker-compose-plugin是Docker Compose的插件版本(V2)。验证安装并设置开机自启:
sudo systemctl start docker sudo systemctl enable docker sudo docker run hello-world运行
hello-world镜像是一个经典测试。如果安装成功,你会看到一串欢迎信息,最后以Hello from Docker!结尾。这证明Docker引擎可以正常拉取镜像并运行容器。
实操心得:安装后,默认只有root用户和sudo用户能运行Docker命令。为了避免每次都要输入sudo,可以将当前用户加入docker用户组:sudo usermod -aG docker $USER。但要注意,这等同于赋予了该用户root权限(因为Docker守护进程以root运行)。在生产环境或多人使用的开发机上需谨慎操作。操作完成后,需要退出当前SSH会话并重新登录,用户组变更才会生效。
2.3 Docker Compose的安装与核心概念
在上一步,我们通过docker-compose-plugin已经安装了Docker Compose V2。它的命令是docker compose(注意中间没有横线)。你可以通过docker compose version来验证。
Docker Compose是一个用于定义和运行多容器Docker应用程序的工具。它的核心是一个YAML格式的配置文件(默认叫docker-compose.yml)。在这个文件里,你可以定义整个应用需要的所有服务(容器)、网络、数据卷等。之后,只需要一个命令docker compose up,就能启动所有定义好的服务。
它的工作流程可以简单理解为:
- 定义:在
docker-compose.yml中描述你的应用架构(需要哪些镜像、端口映射、环境变量、数据卷等)。 - 启动:运行
docker compose up,Compose会帮你拉取镜像(如果本地没有)、创建网络、创建并启动所有容器。 - 管理:使用
docker compose stop/start/restart/down等命令来管理整个应用的生命周期。
相比于手动用docker run启动每一个容器并处理它们之间的链接,Docker Compose极大地简化了多容器应用的管理,是搭建LNMP这类组合环境的绝配。
3. LNMP容器化方案设计与镜像选型
3.1 整体架构设计思路
传统的LNMP是几个软件安装在同一台机器上。在Docker方案里,我们的思路是“一个服务,一个容器”。因此,我们需要三个容器:
- Nginx容器:负责处理HTTP/HTTPS请求,反向代理到后端的PHP处理程序,并托管静态文件。
- PHP-FPM容器:运行PHP-FPM(FastCGI Process Manager)进程,负责解析和执行PHP代码。
- MySQL容器:提供数据库服务。
这三个容器需要能够互相通信。Docker Compose会自动为它们创建一个独立的虚拟网络,容器之间可以通过服务名(service name)作为主机名直接访问,这比手动配置--link或自定义网络要方便和安全得多。
此外,我们还需要考虑数据的持久化:
- MySQL数据:绝对不能保存在容器内部,否则容器删除数据就丢了。需要挂载一个宿主机目录或数据卷(Volume)到容器的数据目录(如
/var/lib/mysql)。 - 网站代码:我们的PHP项目代码也需要挂载到PHP-FPM和Nginx容器中,这样修改宿主机上的代码,容器内能立即生效,非常适合开发。
- Nginx配置:可以将自定义的Nginx站点配置文件挂载到Nginx容器的配置目录。
3.2 官方镜像与自定义镜像选型
对于基础服务,优先使用Docker官方镜像(Official Image)。它们由Docker官方或软件维护团队制作,安全、稳定、更新及时,文档也最全。
- Nginx:我们选择
nginx:alpine。alpine版本基于极简的Alpine Linux,镜像体积非常小(只有几MB),安全性也更高。对于Web服务器,功能完全足够。 - MySQL:选择
mysql:8.0或mysql:latest。MySQL 8.0是目前的主流版本。注意,生产环境建议锁定具体的小版本号,如mysql:8.0.33,以避免自动升级带来的不兼容风险。 - PHP-FPM:选择
php:fpm-alpine。同样,fpm标签表示包含PHP-FPM,alpine表示小体积。我们需要的是带FPM的PHP环境。
但是,官方PHP-FPM镜像通常只包含最基本的PHP核心扩展。我们的项目很可能需要额外的扩展,比如pdo_mysql(连接MySQL)、gd(图像处理)、zip等。因此,我们需要以官方镜像为基础,构建一个自定义的Docker镜像。
构建自定义镜像需要编写一个Dockerfile。它的作用就像一份食谱,告诉Docker如何一步步组装出我们需要的镜像。我们会在Dockerfile里指定基础镜像(php:fpm-alpine),然后运行安装扩展的命令,最后生成一个包含所需扩展的新镜像。
方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 全部使用官方镜像 | 简单快捷,无需构建 | PHP可能缺少必要扩展,需在容器内手动安装(不持久) | 快速测试,或项目仅需PHP核心功能 |
| 自定义PHP镜像 | 扩展预装,环境固化,可版本化管理 | 需要编写和维护Dockerfile,首次需要构建时间 | 推荐。适合所有正式开发和生产环境,确保环境一致性 |
使用第三方集成镜像(如bitnami/lnmp) | 开箱即用,一体化 | 灵活性差,镜像臃肿,学习成本高,更新可能滞后 | 希望快速体验,不关心内部细节 |
显然,为了项目的可维护性和一致性,我们选择“自定义PHP镜像”方案。
4. 详细搭建步骤与配置解析
接下来,我们创建一个项目目录,比如~/docker-lnmp,所有操作都在这个目录下进行。
4.1 创建项目结构与编写Dockerfile
首先,创建必要的目录结构,这有助于文件管理:
mkdir -p ~/docker-lnmp cd ~/docker-lnmp mkdir -p nginx/conf.d php mysql/data wwwnginx/conf.d: 存放自定义的Nginx站点配置文件。php: 存放自定义的PHP Dockerfile及其相关文件。mysql/data: 作为挂载点,持久化MySQL数据库文件。www: 存放我们的PHP项目代码。
现在,我们来创建自定义的PHP镜像。在php目录下创建Dockerfile:
vim ~/docker-lnmp/php/Dockerfile文件内容如下:
# 使用官方PHP-FPM Alpine镜像作为基础 FROM php:8.2-fpm-alpine # 安装系统依赖包 # 这些包是编译PHP扩展或某些功能所必需的 RUN apk add --no-cache \ freetype-dev \ libjpeg-turbo-dev \ libpng-dev \ libzip-dev \ zip \ unzip \ curl \ git \ $PHPIZE_DEPS # 安装并启用常用的PHP扩展 # docker-php-ext-install 是官方镜像提供的便捷安装脚本 RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install -j$(nproc) \ gd \ pdo_mysql \ mysqli \ opcache \ zip \ bcmath \ && docker-php-ext-enable \ opcache # 安装Composer (PHP的依赖管理工具) COPY --from=composer:latest /usr/bin/composer /usr/bin/composer # 设置工作目录 WORKDIR /var/www/html # 提示:默认的www-data用户和组在Alpine中对应的是82:82 # 如果需要调整文件权限,可以在这里修改 # RUN chown -R www-data:www-data /var/www/html这个Dockerfile做了以下几件事:
- 基于
php:8.2-fpm-alpine构建。 - 使用Alpine的包管理器
apk安装编译和运行扩展所需的系统库。 - 使用
docker-php-ext-install编译安装gd(配置了FreeType和JPEG支持)、pdo_mysql、mysqli、opcache、zip、bcmath等扩展。 - 使用多阶段构建,从Composer官方镜像中直接拷贝
composer二进制文件到我们的镜像中,省去了安装步骤。 - 设置容器启动后的默认工作目录。
注意:
$PHPIZE_DEPS是一个包含gcc,make,autoconf等编译工具的包组,安装扩展后,我们可以通过apk del $PHPIZE_DEPS来删除它们以减小镜像体积。但为了后续可能临时安装其他扩展的灵活性,这里选择保留。在生产镜像构建中,可以考虑删除。
4.2 编写Docker Compose核心配置文件
在项目根目录(~/docker-lnmp)创建docker-compose.yml文件:
vim ~/docker-lnmp/docker-compose.yml文件内容如下,我会逐段解释:
version: '3.8' services: # MySQL 服务 mysql: image: mysql:8.0 container_name: lnmp_mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: your_strong_root_password_here # 务必修改! MYSQL_DATABASE: myapp_db MYSQL_USER: myapp_user MYSQL_PASSWORD: your_strong_user_password_here # 务必修改! volumes: - ./mysql/data:/var/lib/mysql - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql # 可选:初始SQL ports: - "3306:3306" networks: - lnmp-network healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"] interval: 10s timeout: 5s retries: 3 # PHP-FPM 服务 (使用自定义镜像) php: build: ./php # 指向包含Dockerfile的目录 container_name: lnmp_php restart: unless-stopped volumes: - ./www:/var/www/html # 挂载项目代码 depends_on: mysql: condition: service_healthy # 等待MySQL健康检查通过 networks: - lnmp-network # Nginx 服务 nginx: image: nginx:alpine container_name: lnmp_nginx restart: unless-stopped ports: - "80:80" - "443:443" # 如需HTTPS,提前准备证书并挂载 volumes: - ./www:/var/www/html:ro # 只读挂载代码目录 - ./nginx/conf.d:/etc/nginx/conf.d:ro # 挂载自定义配置 depends_on: - php networks: - lnmp-network # 定义自定义网络,便于容器间通信 networks: lnmp-network: driver: bridge关键配置解析:
- 版本:
version: '3.8'指定了Compose文件的语法版本。建议使用3.x以上版本以支持更多新特性。 - 服务定义:
mysql:environment: 通过环境变量设置MySQL的root密码、创建默认数据库和用户。这是安全关键点,务必在生产环境中使用强密码,并通过.env文件管理,避免密码硬编码在YAML中。volumes:./mysql/data:/var/lib/mysql将宿主机目录挂载到容器内,实现数据持久化。./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql是一个技巧:如果init.sql文件存在,MySQL容器首次启动时会自动执行它,可用于初始化表结构。ports:"3306:3306"将宿主机的3306端口映射到容器的3306端口,这样你就能用宿主机上的数据库客户端(如Navicat、MySQL Workbench)连接了。如果仅容器间访问,可以去掉此映射,更安全。healthcheck: 配置健康检查,Compose的depends_on的condition选项可以依赖于此,确保PHP服务启动时MySQL已就绪。
php:build: ./php: 这是关键!它告诉Compose去./php目录下寻找Dockerfile并构建镜像。首次运行docker compose up时会自动构建。volumes:./www:/var/www/html将本地项目代码目录挂载到PHP容器的网站根目录,实现代码实时同步。depends_on: 设置依赖关系,condition: service_healthy确保等待MySQL健康状态为“健康”后再启动PHP。
nginx:volumes: 挂载了两处。一是代码目录(只读,:ro),因为Nginx只需要读取静态文件和将PHP请求转发给PHP-FPM。二是自定义配置目录./nginx/conf.d,里面可以放我们自己的站点配置文件,它会覆盖容器内默认的/etc/nginx/conf.d/下的配置。ports: 映射了80和443端口,用于HTTP和HTTPS访问。
- 网络:
networks定义了一个名为lnmp-network的桥接网络。所有服务都加入此网络后,它们可以通过服务名(如mysql,php)直接相互访问,无需知道IP地址。例如,PHP代码中连接数据库的主机名就可以写mysql。
4.3 配置Nginx与PHP通信
Nginx需要通过FastCGI协议将PHP请求转发给PHP-FPM容器。我们需要创建一个Nginx的站点配置文件。
在~/docker-lnmp/nginx/conf.d目录下创建文件default.conf(这个文件名任意,但后缀必须是.conf):
vim ~/docker-lnmp/nginx/conf.d/default.conf内容如下:
server { listen 80; server_name localhost; # 或你的域名 root /var/www/html; index index.php index.html index.htm; # 日志配置(可选,方便调试) access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; location / { try_files $uri $uri/ /index.php?$query_string; } # 处理PHP请求 location ~ \.php$ { fastcgi_pass php:9000; # 关键!php是Compose中定义的服务名 fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # 禁止访问 .htaccess 等隐藏文件 location ~ /\.ht { deny all; } }核心配置解析:
root /var/www/html;: 这是Nginx容器内的路径,因为我们通过卷挂载,它对应着宿主机的./www目录。fastcgi_pass php:9000;: 这是最关键的配置。它告诉Nginx将PHP请求转发到名为php的主机的9000端口。这里的php就是在docker-compose.yml中定义的PHP服务名。Docker的网络DNS会自动将其解析为PHP容器的IP地址。fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;: 这个参数告诉PHP-FPM要执行的具体脚本文件路径,必须正确设置,否则会出现File not found.错误。
4.4 编写测试项目与启动验证
现在,我们在www目录下创建一个最简单的PHP项目来测试环境。
创建入口文件index.php:
vim ~/docker-lnmp/www/index.php内容如下:
<?php phpinfo(); // 尝试连接MySQL数据库 $host = 'mysql'; // 使用服务名 $dbname = 'myapp_db'; $user = 'myapp_user'; $pass = 'your_strong_user_password_here'; // 替换为docker-compose.yml中设置的密码 try { $pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $user, $pass); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); echo "<h2 style='color: green;'>MySQL 连接成功!</h2>"; } catch (PDOException $e) { echo "<h2 style='color: red;'>MySQL 连接失败: </h2>" . $e->getMessage(); } ?>这个脚本做了两件事:显示phpinfo()信息;尝试用我们在docker-compose.yml中配置的数据库信息连接MySQL。
万事俱备,启动所有服务!
在项目根目录(~/docker-lnmp)下,运行:
docker compose up -dup: 创建并启动所有服务。-d: 后台运行(detached mode)。
首次运行会经历以下步骤:
- 为
php服务构建自定义镜像(根据Dockerfile)。 - 为所有服务拉取所需的官方镜像(nginx:alpine, mysql:8.0)。
- 创建
lnmp-network网络。 - 按依赖顺序启动容器:mysql -> php -> nginx。
使用以下命令查看容器状态和日志:
docker compose ps # 查看各服务状态 docker compose logs # 查看所有服务的日志 docker compose logs nginx # 只看nginx服务的日志如果一切正常,docker compose ps应该显示所有容器的状态为Up。
现在,打开浏览器,访问你的服务器IP地址(或http://localhost,如果你在本地虚拟机操作)。你应该能看到标准的phpinfo()页面,并且下方显示“MySQL 连接成功!”的绿色字样。
恭喜!一个基于Docker的LNMP环境已经成功运行。
5. 进阶配置、优化与日常管理
5.1 数据持久化与备份策略
数据是无价的。我们的配置已经通过volumes将MySQL数据持久化到了宿主机的./mysql/data目录。但还需要考虑备份。
MySQL数据备份: 你可以直接备份宿主机上的./mysql/data目录,但更推荐使用mysqldump命令在容器内执行逻辑备份,这样更安全、兼容性更好。
# 进入mysql容器执行备份 docker exec lnmp_mysql mysqldump -u root -p你的root密码 myapp_db > ~/backup_$(date +%Y%m%d_%H%M%S).sql # 或者使用docker compose docker compose exec mysql mysqldump -u root -p你的root密码 --all-databases > ~/full_backup.sql可以将此命令加入crontab实现定时备份。
项目代码备份:./www目录本身就在宿主机,常规文件备份方式即可。
使用Docker命名卷(Named Volume):对于生产环境,更推荐使用Docker管理的命名卷,而不是绑定挂载(./mysql/data)。命名卷由Docker完全管理,性能更好,且更容易迁移。修改docker-compose.yml中MySQL的卷配置:
volumes: - mysql_data:/var/lib/mysql # 替换原来的 ./mysql/data # 在文件底部 volumes: 部分声明 volumes: mysql_data:然后通过docker volume ls和docker volume inspect lnmp_mysql_data来管理卷。
5.2 性能调优与安全加固
1. 资源限制: 在docker-compose.yml中,可以为每个服务设置CPU和内存限制,防止某个容器耗尽主机资源。
services: mysql: # ... 其他配置 deploy: # 注意:在Compose v3中,resources放在deploy下 resources: limits: cpus: '1.0' # 最多使用1个CPU核心 memory: 1G # 最多使用1GB内存 reservations: cpus: '0.5' memory: 512M对于非Swarm模式,可以使用cpus和mem_limit等旧参数,但deploy.resources是更现代的方式(在docker compose up时生效,需Compose V2)。
2. PHP-FPM调优: PHP-FPM的配置位于容器内的/usr/local/etc/php-fpm.d/www.conf。我们可以通过挂载自定义配置文件来覆盖默认值。首先在宿主机创建配置文件:
mkdir -p ~/docker-lnmp/php/php-fpm.d vim ~/docker-lnmp/php/php-fpm.d/zz-docker.conf内容示例(根据服务器配置调整):
[www] pm = dynamic pm.max_children = 50 pm.start_servers = 5 pm.min_spare_servers = 5 pm.max_spare_servers = 35 pm.max_requests = 500然后在docker-compose.yml中PHP服务的volumes部分添加挂载:
volumes: - ./www:/var/www/html - ./php/php-fpm.d/zz-docker.conf:/usr/local/etc/php-fpm.d/zz-docker.conf3. 安全加固:
- 修改默认端口:将
docker-compose.yml中MySQL的端口映射改为"3307:3306",避免使用默认的3306端口,减少被扫描的风险。 - 使用
.env文件管理密码:创建.env文件(确保在.gitignore中),将密码写入:
在MYSQL_ROOT_PASSWORD=your_very_strong_password MYSQL_PASSWORD=another_strong_passworddocker-compose.yml中引用:MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}。 - Nginx禁用服务器令牌:在Nginx配置中添加
server_tokens off;,隐藏Nginx版本信息。 - 容器用户非root运行:在
Dockerfile中,可以使用USER指令指定一个非root用户来运行进程,例如在PHP的Dockerfile末尾添加USER www-data。
5.3 常用Docker Compose管理命令
掌握这些命令,你就能轻松管理整个LNMP环境:
# 启动服务(后台模式) docker compose up -d # 停止服务 docker compose down # 停止服务并删除所有相关容器、网络(不会删除镜像和卷) docker compose down # 停止服务并删除容器、网络、以及docker-compose.yml中定义的匿名卷(谨慎!) docker compose down -v # 查看运行状态 docker compose ps # 查看实时日志 docker compose logs -f # 查看某个服务的日志 docker compose logs -f nginx # 在运行中的容器内执行命令(例如进入MySQL命令行) docker compose exec mysql mysql -u root -p # 重新构建并启动服务(例如修改了Dockerfile后) docker compose up -d --build # 重启某个服务 docker compose restart php # 暂停和恢复服务 docker compose pause docker compose unpause5.4 项目部署与更新流程
当你的PHP项目开发完成,需要更新到线上环境时,流程非常简单:
- 更新代码:将最新的代码上传到宿主机的
./www目录(或通过Git拉取)。 - 更新依赖:如果你的项目使用Composer,可以在PHP容器内执行更新:
docker compose exec php composer install --no-dev --optimize-autoloader - 执行数据库迁移:如果有Laravel之类的框架,需要运行迁移:
docker compose exec php php artisan migrate --force - 清理缓存:清理应用缓存、视图缓存等。
docker compose exec php php artisan optimize:clear - 重启服务(如有必要):通常代码更新无需重启容器,因为目录是挂载的。但如果修改了PHP环境(如
.env文件)或需要重启FPM进程,可以重启PHP服务:
如果修改了Nginx配置,需要重启或重载Nginx:docker compose restart phpdocker compose exec nginx nginx -s reload # 平滑重载,不影响在线服务
整个更新过程无需停机,体现了容器化部署的巨大优势。
6. 常见问题排查与实战技巧
即使按照步骤操作,也可能会遇到问题。这里记录了一些我踩过的坑和解决方法。
6.1 容器启动失败与日志分析
问题:运行docker compose up -d后,使用docker compose ps发现某个容器状态是Exit或Restarting。
解决:这是最常遇到的问题。第一步永远是查看日志。
docker compose logs [service_name] # 查看特定服务日志 docker compose logs --tail=50 -f # 查看最后50行并实时跟随- MySQL启动失败:常见原因是宿主机上
./mysql/data目录的权限问题。确保目录存在且MySQL容器内的mysql用户(通常是999:999)有读写权限。可以尝试sudo chown -R 999:999 ./mysql/data。也可能是旧数据与新版本不兼容。 - PHP构建失败:查看构建日志。常见原因是网络问题导致
apk add或docker-php-ext-install下载包失败。可以尝试更换Alpine的软件源,或者在Dockerfile中增加构建超时和重试逻辑。 - Nginx启动失败:通常是
default.conf配置文件有语法错误。可以通过命令测试配置:docker compose exec nginx nginx -t。
6.2 网络连接问题:502 Bad Gateway
问题:浏览器访问显示502错误,Nginx日志(docker compose logs nginx)显示connect() failed (111: Connection refused) while connecting to upstream。
解决:这几乎总是Nginx无法连接到PHP-FPM服务。
- 检查PHP-FPM容器是否运行:
docker compose ps,确保php服务状态为Up。 - 检查Nginx配置中的
fastcgi_pass:确认配置文件中写的是php:9000(服务名+端口),并且端口是PHP-FPM监听的端口(默认9000)。 - 检查PHP-FPM监听设置:进入PHP容器检查
/usr/local/etc/php-fpm.d/www.conf,确认listen = 9000,并且listen.allowed_clients注释掉或包含Nginx的IP。 - 检查Docker网络:确保
nginx和php服务在同一个自定义网络(lnmp-network)中。使用docker network inspect lnmp-network查看容器连接情况。
6.3 文件权限与目录挂载问题
问题:PHP应用无法写入日志文件、上传文件,或者Nginx报Permission denied错误。
解决:这是宿主机用户ID(UID)与容器内用户ID不匹配导致的经典问题。容器内PHP-FPM和Nginx通常以www-data用户(UID 82)运行,而宿主机上的./www目录所有者可能是你的当前用户(如UID 1000)。
- 方案一(简单):在宿主机上修改项目目录权限为
777(不推荐用于生产):chmod -R 777 ./www。 - 方案二(推荐):让容器内的进程以宿主机当前用户的UID运行。修改PHP的
Dockerfile,在最后添加:
并将RUN apk add --no-cache shadow && \ usermod -u 1000 www-data && \ groupmod -g 1000 www-data./www目录的UID/GID改为1000。同时,在docker-compose.yml的PHP和Nginx服务中,添加用户映射:
这样容器内的进程就会以UID 1000运行,与宿主机用户匹配。user: "1000:1000"
6.4 性能问题与调试技巧
问题:网站访问速度慢,数据库连接超时。
解决:
- 查看容器资源使用:
docker stats可以实时查看所有容器的CPU、内存使用情况。 - 进入容器调试:
在容器内,你可以运行docker compose exec php sh # 进入PHP容器 docker compose exec mysql bash # 进入MySQL容器top、free -m,或者使用mysql命令行工具检查数据库状态。 - 分析慢查询:在MySQL容器内,启用慢查询日志或在PHP应用中配置数据库调试模式。
- 调整PHP-FPM配置:如前所述,根据服务器硬件情况调整
pm.max_children等参数。一个粗略的计算公式:max_children = 可用内存 / 单个PHP进程平均内存占用。
6.5 镜像构建优化与清理
随着开发进行,会产生很多中间镜像和停止的容器,占用磁盘空间。
- 清理所有停止的容器、未使用的网络、构建缓存:
docker system prune -f - 清理未使用的镜像:
docker image prune -a - 优化Dockerfile构建:利用Docker的构建缓存。将不经常变动的操作(如安装系统包)放在
Dockerfile前面,将经常变动的操作(如拷贝应用代码)放在后面。合并RUN命令以减少镜像层数。
我个人在实际操作中的体会是,Docker化LNMP环境最大的价值不在于第一次搭建的速度,而在于其带来的确定性和可移植性。这份docker-compose.yml和相关的配置文件,就是你的环境“蓝图”。无论是换一台新电脑,还是在新服务器上部署,只需要git clone代码和这份配置文件,然后一个docker compose up -d,一个完全一致的环境就在几分钟内准备就绪。它把环境配置从一门“手艺”变成了可版本控制的“代码”,这对于团队协作和持续集成/持续部署(CI/CD)来说是革命性的。最后一个小技巧,可以把常用的docker compose命令写成Makefile或Shell脚本别名,进一步提升效率。