MeEdu 项目学习笔记:PHP、Laravel、FastCGI、Meilisearch 与 Docker 部署¶
本文根据与 ChatGPT 的讨论,并结合
Qsnh/meedu仓库中的 README、Dockerfile、Nginx 配置、PHP-FPM 配置、Laravel 路由及搜索相关代码整理。项目地址:https://github.com/Qsnh/meedu
参考时间:2026-10-19
说明:本文尽量区分“项目当前代码里能确认的事实”和“便于理解的简化示例”。项目依赖版本、部署配置可能随仓库后续更新而变化。
1. MeEdu 是什么¶
MeEdu 是一个面向网校、在线课程和知识付费场景的项目。仓库 README 将其描述为基于 PHP 7.4 + Laravel 8 + MySQL + Redis 的网校解决方案。项目采用前后端分离架构,包括 PC、H5、管理后台等前端项目,以及提供 HTTP API 的 PHP 后端。
主要技术组成:
| 组件 | 在 MeEdu 中的用途 |
|---|---|
| PHP 7.4 | 执行后端业务代码 |
| Laravel 8 | PHP 后端框架,负责路由、请求处理、业务代码组织等 |
| PHP-FPM | 管理 PHP 工作进程,接收 FastCGI 请求 |
| Nginx | 提供静态前端文件、接收 HTTP 请求、把 PHP 请求转给 PHP-FPM |
| MySQL | 业务数据存储 |
| Redis | 缓存及项目配置的相关用途 |
| Meilisearch | 全文搜索服务 |
| Node.js + pnpm | 构建前端项目;构建完成后通常不需要在运行容器中持续运行 Node.js 来提供这些静态页面 |
| Docker / Docker Compose | 构建镜像并组合运行应用、数据库、缓存和搜索服务 |
需要先建立一个概念:Laravel 是框架,PHP 是语言和运行时,PHP-FPM 是请求处理进程管理器,Nginx 是 Web 服务器。它们不是同一个东西。
2. 整体部署架构¶
MeEdu 把几个前端项目编译成静态资源,放进应用容器;API 由 Laravel 执行。MySQL、Redis、Meilisearch 则分别作为独立容器运行。
浏览器
┌──────────────┼──────────────┐
│ │ │
PC 页面 H5 页面 管理后台
│ │ │
└──────────────┼──────────────┘
│ HTTP
┌──────▼──────┐
│ MeEdu 容器 │
│ Nginx │
│ ┌────────┐ │
│ │静态文件│ │
│ └────────┘ │
│ │ API │
│ FastCGI │
│ ▼ │
│ PHP-FPM │
│ │ │
│ Laravel │
└──────┬───────┘
│ Docker 内部网络
┌───────────┼────────────┐
▼ ▼ ▼
MySQL Redis Meilisearch
仓库的 docker/nginx/default.conf 配置了四个监听端口:
| 端口 | 用途 |
|---|---|
8000 |
API,Nginx 根目录指向 /var/www/api/public |
8100 |
PC 前端静态站点 |
8200 |
H5 前端静态站点 |
8300 |
管理后台静态站点 |
PC、H5、管理后台的编译产物都由 Nginx 提供。当这些前端请求 /api/ 路径时,Nginx 会反向代理给同一容器内的 API 服务。
重要:三个前端是三个前端项目,不代表运行时必须有三个 Node.js 服务。 如果它们已经编译成 HTML、CSS、JavaScript 等静态文件,Nginx 直接读取并返回这些文件即可。
3. PHP、Laravel 与 Spring Boot 的关系¶
可以把 Laravel 理解成 PHP 生态中承担类似 Spring Boot 角色的后端开发框架:它帮助开发者组织 HTTP 路由、Controller、数据库访问、中间件、依赖注入等功能。但它们具体的运行时与应用生命周期不同。
3.1 一个简单的 API 对比¶
目标是提供:
返回 JSON:
Spring Boot 中可能这样写:
@RestController
@RequestMapping("/api/courses")
public class CourseController {
@GetMapping
public List<Course> list() {
return List.of(
new Course(1L, "PHP 入门"),
new Course(2L, "Java 入门")
);
}
}
Laravel 中则通常把路由定义与控制器分开。下面只是简化示例,不是 MeEdu 的实际课程接口代码:
class CourseController extends Controller
{
public function index()
{
return response()->json([
['id' => 1, 'name' => 'PHP 入门'],
['id' => 2, 'name' => 'Java 入门'],
]);
}
}
3.2 常见概念映射¶
| Java 常见组件 / 概念 | PHP / Laravel 中相近的概念 |
|---|---|
| Spring Boot | Laravel 应用框架(角色相似,并非运行机制完全相同) |
@RestController |
Controller 类 + 返回 JSON 的响应 |
@GetMapping |
Route::get() |
| Spring IoC / 依赖注入 | Laravel Service Container / 依赖注入 |
| JPA / MyBatis | Eloquent ORM 或直接数据库访问 |
| Maven / Gradle | Composer(PHP 依赖管理) |
| Spring Boot CLI / 相关命令 | Artisan(Laravel 命令行工具) |
3.3 PHP 不是“每次请求都启动一个全新的操作系统进程”¶
这是理解 PHP 的关键:传统 PHP-FPM 模式下,PHP-FPM 工作进程是长期存在的,可以连续处理多个请求。但在传统请求生命周期中,Laravel 应用通常会对每个请求重新执行框架引导流程、构造请求上下文、运行路由和业务逻辑,再生成响应。
因此要区分两件事:
- 常驻工作进程:进程一直存在,等待下一次请求。
- 常驻应用实例:框架应用对象长时间驻留并在多个请求之间复用。
PHP-FPM 具备前者,但传统 Laravel + PHP-FPM 并不等同于 Java Web 应用通常会持续复用 JVM 中应用对象的方式。Laravel Octane 等方案可以使用常驻应用服务器模式,但会带来跨请求状态和内存管理方面的额外要求。
4. CGI、FastCGI 与 PHP-FPM¶
4.1 CGI 是什么¶
CGI 是 Common Gateway Interface(通用网关接口)。早期 Web 服务器主要负责返回静态文件;如果要动态查询数据、计算价格或生成网页,就需要调用外部程序。CGI 定义了 Web 服务器如何把请求信息传给程序,以及如何读取程序的响应。
典型 CGI 程序可以通过以下方式获取信息:
- 环境变量:如请求方法、查询字符串、客户端 IP。
- 标准输入(STDIN):例如 POST 请求正文。
- 标准输出(STDOUT):输出响应头与响应正文。
传统 CGI 的典型运行方式是:每个请求启动一个外部程序,程序处理完请求后退出。CGI 不限定语言,C、Perl、Python、Shell 等都可以实现符合规范的 CGI 程序。
4.2 FastCGI 改进了什么¶
FastCGI 是一种 Web 服务器与后端程序之间的通信协议。相较于传统 CGI 的典型“一次请求启动一次程序”的方式,FastCGI 允许后端程序持续运行,反复接收请求。
因此,FastCGI 的核心优势是:把后端进程生命周期与单个 HTTP 请求分离,避免反复创建进程的成本。
4.3 PHP-FPM 是什么¶
PHP-FPM 是 PHP 的 FastCGI Process Manager(FastCGI 进程管理器),负责维护 PHP 工作进程池、接收 FastCGI 请求和管理工作进程的生命周期。它不是 Laravel,也不是 FastCGI 协议本身。
可以这样记:
| 名称 | 本质 |
|---|---|
| CGI | Web 服务器调用外部程序的一套接口规范 |
| FastCGI | Web 服务器与后端程序之间的通信协议 |
| PHP-FPM | PHP 的 FastCGI 进程管理器 / 服务实现 |
| Laravel | 在 PHP 上运行的后端开发框架 |
4.4 MeEdu 中 Nginx 与 PHP-FPM 如何连接¶
docker/nginx/default.conf 中有类似配置:
location ~ [^/]\.php(/|$) {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
而 PHP-FPM 的 docker/php/php-fpm.d/www.conf 中配置了:
两者对应起来了:Nginx 将 PHP 脚本请求交给监听本机 9000 端口的 PHP-FPM。
注意:9000 端口不是普通 HTTP 端口。浏览器通常不能直接用 HTTP 访问 PHP-FPM 的这个端口,因为它使用的是 FastCGI 协议。
4.5 FastCGI 会不会让 PHP 比 Java 慢¶
不能简单地这么推断。FastCGI 是 Nginx 与 PHP-FPM 之间的内部通信协议,通常不是 Web 应用性能差异的唯一或主要原因。Java 服务也常部署在 Nginx 后面,通过 HTTP 反向代理转发请求。
性能差异还取决于:
- PHP 和 Java 的运行时、框架及代码执行方式;
- Laravel 的请求引导成本与是否使用 OPcache;
- JVM 的 JIT 优化;
- 并发模型和进程 / 线程数量;
- 数据库、缓存、远程 API 的耗时;
- 业务逻辑和部署资源。
对于数据库耗时占主导的常规 CRUD 服务,两者差异可能不明显;CPU 密集型或高并发短请求场景则需要使用相同业务、数据、硬件和压测方法实际比较。
5. Laravel 的 HTTP 请求处理流程¶
MeEdu 的 xyz.meedu.api/public/index.php 是 Laravel 的应用入口。它看起来很短,因为它不是放业务逻辑的地方,而是负责启动框架并将请求交给 Laravel 内核。
主要逻辑可以概括为:
// 1. 注册 Composer 自动加载
require __DIR__ . '/../vendor/autoload.php';
// 2. 引导 Laravel 应用
$app = require_once __DIR__ . '/../bootstrap/app.php';
// 3. 从 HTTP 环境中捕获请求,并获取 Laravel HTTP Kernel
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
// 4. 交给内核处理请求
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
// 5. 发送响应,然后执行终止阶段处理
$response->send();
$kernel->terminate($request, $response);
可以理解为:
Nginx 收到请求
↓
发现不是实际静态文件,转到 /index.php
↓
PHP-FPM 执行 public/index.php
↓
Laravel 启动 / 加载路由和中间件
↓
根据 HTTP 方法 + URL 匹配路由
↓
调用 Controller / Service / 数据访问层
↓
生成 JSON 或其他 HTTP 响应
↓
经 Nginx 返回浏览器
因此,打开 public/index.php 看不到课程业务,是正常的。要理解具体 API,需要继续看路由文件、Controller、Service 和数据访问代码。
6. MeEdu 的路由和前后端分离¶
6.1 API 不要求 URL 以 .php 结尾¶
例如:
这些是普通 HTTP API 路径,不需要带 .php。Nginx 中对 .php 的规则是为了将 PHP 脚本交给 PHP-FPM;它不表示只有 .php 结尾的 URL 才是 API。
6.2 为什么 /api/v2/courses 最终会执行 index.php¶
MeEdu Nginx 的 API 服务配置中包含:
try_files 会先检查请求是否对应真实文件或目录;如果没有,就内部转发到 index.php,并保留查询参数。然后 Nginx 的 PHP 规则把 index.php 交给 PHP-FPM。
浏览器的地址栏仍然是 /api/v2/courses,不会因为内部转发而显示为 /index.php。
6.3 Laravel 如何知道请求的是 /api/v2/courses¶
这是理解 Laravel 请求链路的关键:Nginx 转发给 PHP-FPM 的是固定入口脚本 index.php,但原始 HTTP 请求的方法、URI、查询参数等信息仍然会通过 FastCGI 参数传给 PHP。Laravel 再把这些信息封装成 Request 对象,并用它们匹配路由。
假设客户端发送:
这条请求至少包含以下信息:
| 信息 | 示例值 |
|---|---|
| HTTP 方法 | GET |
| 请求 URI | /api/v2/courses?page=1 |
| 查询字符串 | page=1 |
| 入口 PHP 文件 | public/index.php |
第一步:Nginx 改变执行目标,不是抹掉原始请求¶
API Nginx 配置中的规则:
会先检查请求是否对应实际存在的文件或目录;如果不存在,就内部转到 index.php。例如执行目标会变为 /index.php?page=1,但原始请求 URI 通常仍可通过 $request_uri / FastCGI 的 REQUEST_URI 获取。浏览器地址栏也仍显示 /api/v2/courses?page=1。
第二步:FastCGI 把请求信息交给 PHP-FPM¶
配置中的:
fastcgi_pass 127.0.0.1:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
告诉 Nginx 将 PHP 请求交给 PHP-FPM,并传入要执行的脚本路径及请求环境参数。常见 FastCGI 参数包括:
| 参数 | 典型值 | 用途 |
|---|---|---|
REQUEST_METHOD |
GET |
HTTP 方法 |
REQUEST_URI |
/api/v2/courses?page=1 |
原始请求 URI,通常含查询字符串 |
QUERY_STRING |
page=1 |
问号后的查询字符串 |
SCRIPT_FILENAME |
/var/www/api/public/index.php |
PHP-FPM 实际执行的脚本文件 |
具体有哪些参数、如何定义,要以服务器上 include fastcgi_params 引入的实际文件为准。关键是:SCRIPT_FILENAME 表示“执行哪个 PHP 文件”,而 REQUEST_URI 表示“客户端请求的哪个 URI”,两者作用不同。
第三步:PHP 提供请求环境,Laravel 创建 Request 对象¶
在 PHP 代码中,可以读取请求环境变量:
$_SERVER['REQUEST_METHOD']; // 例如 GET
$_SERVER['REQUEST_URI']; // 例如 /api/v2/courses?page=1
$_SERVER['QUERY_STRING']; // 例如 page=1
$_GET['page']; // 例如字符串 "1"
Laravel 的入口文件 public/index.php 会调用类似下面的代码(具体写法随 Laravel 版本略有差异):
Request::capture() 会依据当前 PHP 请求环境构造 Laravel Request 对象。之后可以通过对象方法读取信息,例如:
$request->method(); // GET
$request->path(); // api/v2/courses(不带开头的斜杠)
$request->query('page'); // 1
$request->fullUrl(); // 完整 URL,通常包含查询参数
这一步只是把请求信息封装起来;Request::capture() 本身并不负责决定调用哪个控制器。
第四步:Laravel 路由系统按 HTTP 方法和路径匹配¶
$kernel->handle($request) 会进入 Laravel 的 HTTP 请求处理流程。经过相关中间件后,路由系统会根据请求的 HTTP 方法、URI 等信息,与已经注册的路由进行匹配。
例如,下面是一个用于解释原理的简化示例,不代表 MeEdu 源码中的实际控制器名称:
如果这条路由注册在 /api/v2 前缀的路由组中,那么对外完整路径就是:
匹配成功后,Laravel 才会调用对应的控制器方法;控制器再执行业务逻辑并返回响应。
放回 MeEdu 项目中看¶
MeEdu 并不是简单地把所有 API 都写在默认的 routes/api.php 中。它通过 app/Providers/RouteServiceProvider.php 注册不同的路由文件,其中 routes/frontend-v2.php 对应前台 V2 API,路由组添加 /api/v2 前缀。因此,可以沿着以下路径查找真实接口:
- 查看
xyz.meedu.api/docker/nginx/default.conf(仓库中的对应路径是docker/nginx/default.conf),确认 Nginx 的try_files和 FastCGI 配置。 - 查看
xyz.meedu.api/public/index.php,理解 Laravel 的入口。 - 查看
xyz.meedu.api/app/Providers/RouteServiceProvider.php,确认路由文件注册和前缀。 - 查看
xyz.meedu.api/routes/frontend-v2.php,找到/courses对应的路由定义。 - 沿着路由指向的控制器继续阅读业务代码。
也可以在项目后端目录执行:
如果当前 Laravel Artisan 版本支持路径筛选,也可以尝试:
这样可以核对实际注册的 HTTP 方法、URI 和处理方法,而不是仅凭文件名猜测。
6.4 MeEdu 实际用哪些路由文件¶
不要机械套用所有 Laravel 项目默认的 routes/api.php。检查 MeEdu 的 app/Providers/RouteServiceProvider.php 可以看到,它把路由按用途拆分到了多个文件:
| 路由文件 | 路由前缀 / 用途 |
|---|---|
routes/frontend.php |
/api,旧版 / 前台 API 与部分回调 |
routes/frontend-v2.php |
/api/v2,前台 API V2 |
routes/frontend-v3.php |
/api/v3,前台 API V3 |
routes/backend-v1.php |
/backend/api/v1,后台 API V1 |
routes/backend-v2.php |
/backend/api/v2,后台 API V2 |
routes/web.php |
网站页面、支付回调等 Web 路由,使用 web 中间件 |
比如 routes/frontend-v2.php 中可看到 /courses、/course/{id}、/course_categories 等路由。结合 RouteServiceProvider 中的 /api/v2 前缀,对外路径就是 /api/v2/courses、/api/v2/course/{id} 等。
6.5 直接用浏览器访问 API 可以吗¶
可以。浏览器访问的是常规 HTTP 接口:
- GET 接口可以直接在地址栏输入;
- POST / PUT / DELETE 接口通常需要发送请求体,可以用浏览器开发者工具、curl 或 Postman;
- 需要登录的接口可能要求 JWT 或其他身份认证;
- 公开 GET 接口可能返回 JSON,但并非所有接口都允许匿名访问。
注意:真实的接口路径应以当前代码里的路由定义为准;示例路径只用于解释原理。
7. Meilisearch 是什么,在项目中怎么用¶
7.1 它是什么¶
Meilisearch 是独立的全文搜索引擎,擅长对文本数据建立搜索索引并根据关键词快速返回匹配结果。它支持相关性排序、过滤与拼写容错等搜索特性。
它不是:
- 不是 PHP 语言;
- 不是 Laravel 框架;
- 不是 MySQL 的替代品;
- 不是一个只在 Laravel 进程内部运行的库。
它是一个单独运行的服务,通过 HTTP API 接收索引和搜索请求。MeEdu 的 Docker Compose 为它分配了独立容器与数据卷。
7.2 为什么不只用 MySQL 的 LIKE¶
简单搜索可以用 MySQL 查询,例如:
但对于跨多个文本字段检索、相关性排序、拼写容错、搜索体验优化等需求,专门的搜索引擎通常更合适。是否需要独立搜索引擎仍取决于数据规模与业务需要,小项目并非一开始就必须部署它。
7.3 MeEdu 的实际配置¶
仓库 compose.yml 中有单独的 meilisearch 服务,大意如下:
meilisearch:
image: registry.cn-hangzhou.aliyuncs.com/hzbs/meilisearch:0.24.0
restart: always
volumes:
- data_meilisearch:/meili_data
项目根目录 .env.example 里可以看到:
meilisearch 是 Compose 网络内的服务名,7700 是该服务使用的端口。这个地址一般是容器之间访问的地址,不等同于公开给浏览器访问的地址。
7.4 Laravel Scout 的作用¶
MeEdu 使用 Laravel Scout 以及 PHP 客户端 meilisearch/meilisearch-php 来集成搜索。
xyz.meedu.api/config/scout.php 中配置了:
并通过 MEILISEARCH_HOST、MEILISEARCH_KEY 配置连接信息。
Laravel Scout 相当于 Laravel 应用与搜索引擎之间的一层集成 API,简化模型数据同步和搜索调用。
7.5 MeEdu 的搜索模型和服务¶
相关代码包括:
SearchRecord.php:模型使用Laravel\Scout\Searchable,数据库表为search_records。FullSearchService.php:包含新增 / 更新 / 删除搜索记录、执行搜索等业务方法。OtherDao.php:通过SearchRecord::search($keywords)->take($limit)->get()取搜索结果。config/scout.php:指定搜索引擎驱动、索引前缀和连接配置。
代码调用形态类似:
使用 Scout 配置后,搜索请求会交给配置的 Meilisearch 驱动处理。
MeEdu 还会将待搜索内容保存为 search_records 数据,并在新增、更新、删除业务搜索记录时同步索引。这样 MySQL 里的业务数据与 Meilisearch 的搜索索引各司其职。
需要注意:项目中有多个版本的搜索 API 及服务。不要仅凭 /api/v2/search 这样的路由名称,就断言该路由一定使用上述 FullSearchService;判断具体接口用了哪个搜索实现,应沿着该接口 Controller 和 Service 的调用链继续读源码。
7.6 MySQL 和 Meilisearch 为什么会有重复数据¶
这是正常的设计:
- MySQL 保存业务数据,并可保存待搜索的记录;
- Meilisearch 保存针对检索优化的索引;
- 当标题或简介变化时,应用需要确保搜索记录 / 搜索索引同步更新;
- 搜索引擎索引不是业务数据的唯一真实来源。
在生产部署中,搜索服务的 API Key 需要妥善保管,不应随意将有管理权限的密钥暴露给前端浏览器或公开网络。
8. Docker 多阶段构建:多个 FROM 是什么含义¶
MeEdu 的 Dockerfile 使用了多阶段构建(Multi-stage Build)。简化后,关键结构如下:
# 阶段 1:前端构建环境
FROM registry.cn-hangzhou.aliyuncs.com/hzbs/node:20-alpine AS node-base
# 复制前端源码,安装依赖,分别构建 admin、pc、h5
# ... pnpm install / pnpm build ...
# 阶段 2:最终运行环境
FROM registry.cn-hangzhou.aliyuncs.com/hzbs/php:7.4-fpm-alpine AS base
# PHP 配置、Nginx 配置和 Laravel 后端源码
COPY docker/nginx/default.conf /etc/nginx/http.d/default.conf
COPY --chown=www-data:www-data xyz.meedu.api /var/www/api
# 从第一阶段只复制前端构建产物
COPY --from=node-base /app/admin/dist /var/www/admin
COPY --from=node-base /app/pc/dist /var/www/pc
COPY --from=node-base /app/h5/dist /var/www/h5
以上仅为结构示意;实际 Dockerfile 还执行了依赖安装、权限设置、Laravel Artisan 命令等操作。
8.1 多个 FROM 为什么可以写在一个 Dockerfile¶
每个 FROM 都会开始一个构建阶段。
- 第一阶段使用 Node.js 环境,安装前端依赖并生成静态构建产物;
- 第二阶段从 PHP-FPM 镜像开始,准备最终运行环境;
COPY --from=node-base ...将第一阶段的构建产物复制到第二阶段。
第二个 FROM 不会自动继承第一个阶段里的 Node.js、node_modules 等文件。只有显式 COPY --from=... 的内容才会被复制。
默认情况下,最终镜像是 Dockerfile 最后一个构建阶段的结果。多个 FROM 不表示最终会自动生成多个容器。
8.2 为什么要这样构建¶
前端编译需要 Node.js、pnpm 和开发依赖,但部署静态文件并不需要保留整个构建环境。多阶段构建可将构建工具留在前一阶段,只把最终产物带入运行镜像,从而简化最终环境并减少不必要的内容。
这也解释了为什么 MeEdu 的最终应用容器可以包含多个前端网站的静态文件,却不需要启动多个 Node.js Web 服务。
9. Docker Compose:运行时有哪些容器¶
仓库根目录的 compose.yml 主要定义了四个服务:
| Compose 服务 | 主要用途 | 持久化 / 网络要点 |
|---|---|---|
meedu |
应用容器,镜像为 registry.cn-hangzhou.aliyuncs.com/meedu/light:4.9.32 |
对外发布 8000、8100、8200、8300 端口 |
mysql |
MySQL 业务数据库 | 使用 data_mysql 卷保存数据库文件;未直接发布宿主机端口 |
redis |
Redis 服务 | 使用 data_redis 卷保存数据;未直接发布宿主机端口 |
meilisearch |
全文搜索 | 使用 data_meilisearch 卷;Compose 示例未直接发布宿主机端口 |
这些服务处于 Compose 定义的 meedu-network 网络中。应用容器可以通过服务名访问其他容器,例如:
这里使用的是 Docker Compose 内部 DNS 服务名,不是公网 DNS。
9.1 本地 Dockerfile 与 Compose 镜像要区分¶
仓库 Dockerfile 描述如何从源代码构建一个镜像;当前 compose.yml 则直接引用已发布的预构建镜像 meedu/light:4.9.32。
所以,执行 docker compose up -d 时,默认不一定会根据当前本地 Dockerfile 重新构建镜像。若想使用本地源码改动,需按实际需要配置 build:、指定镜像标签,并执行构建命令。
9.2 为什么数据库、Redis、搜索引擎通常拆成独立容器¶
这几个组件生命周期、数据持久化需求和资源特征都不同:
- 可以单独升级或更换;
- 数据卷独立管理;
- 可以单独做资源限制与监控;
- 应用容器重建时,不必连同数据库文件一起重建。
应用容器内则把 Nginx 和 PHP-FPM 合在一起,是 MeEdu 为便于交付而做的部署取舍,并不是 Docker 强制规定必须如此。
10. MeEdu 的 Nginx 配置怎么理解¶
MeEdu 的 docker/nginx/default.conf 把 API 与三个前端站点分别配置成不同的 server。
10.1 API 的 Nginx 规则¶
核心配置逻辑:
server {
listen 8000;
root /var/www/api/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ [^/]\.php(/|$) {
try_files $uri =404;
fastcgi_pass 127.0.0.1:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
location / 的 try_files 先尝试匹配真正存在的文件 / 目录;如果找不到,就把请求内部转到 Laravel 的 index.php。这通常称为单一入口(Front Controller)模式。
所以:
GET /api/v2/courses
↓
Nginx 发现没有名为 /api/v2/courses 的静态文件
↓
内部转发到 /index.php?原查询参数
↓
PHP-FPM 执行 public/index.php
↓
Laravel 路由匹配 /api/v2/courses
↓
执行 Controller / Service
只有 PHP 脚本需要通过 PHP-FPM 执行,不代表只有 .php 结尾的 URL 才是 API。 对外业务路由一般是 /api/v2/courses 这样的友好路径。
10.2 前端 Nginx 规则¶
PC、H5、管理后台的 server 分别监听 8100、8200、8300。它们将对应的静态目录作为 root,并对 /api/ 做类似如下代理:
location /api/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8000/;
}
这里使用的是普通 HTTP 反向代理,因为前端 Nginx 向本机 API Nginx 发送的是 HTTP 请求;再由 API Nginx 使用 FastCGI 与 PHP-FPM 通信。
注意:proxy_pass 的 URL 以 / 结尾时,Nginx 会把 location /api/ 匹配到的前缀替换为 /。结合前端构建时的 VITE_APP_URL=/api/,例如前端实际请求 /api/api/v2/courses,到达 API 服务后会变成 /api/v2/courses。
所以,这个项目里同时存在两种不同通信:
- 浏览器 → Nginx:HTTP / HTTPS;
- 前端站点 Nginx → API Nginx:HTTP;
- API Nginx → PHP-FPM:FastCGI。
不要把 HTTP 反向代理与 FastCGI 混为一谈。
11. 与 Java / Spring Boot 部署方式的对比¶
11.1 典型请求链路¶
MeEdu:
常见 Spring Boot:
也可以在 Java 系统前面放 Nginx:
因此,不应简单地认为 PHP 使用 FastCGI、Java 直接提供 HTTP,所以 FastCGI 必然更慢。更应该比较应用的请求生命周期、运行时、代码、数据库与并发配置。
11.2 部署层面的选择¶
前端与后端打包在一个应用容器:
优点是部署简单,版本可以一起发布;缺点是前端与后端不容易单独扩容或独立发布。
前端、API 分开部署:
优点是可以独立发布、扩容与分配资源;缺点是多出跨服务配置、网络路由、版本兼容等运维工作。
MeEdu 选择将编译后的前端静态文件与 Laravel / Nginx / PHP-FPM 放入同一个应用镜像,而将数据库、Redis、Meilisearch 拆开。这是一个偏向易部署的方案。
12. 值得留意的版本和部署问题¶
12.1 PHP 7.4 已结束官方支持¶
仓库目前使用 PHP 7.4 / Laravel 8,这是理解传统 PHP-FPM 项目的一个案例,但并不意味着新项目应该继续采用该版本。PHP 官方列出的 PHP 7.4 结束支持时间为 2022-11-28。部署或二次开发时,应检查 PHP、Laravel 及其依赖的兼容性,并规划升级。
官方版本状态:https://www.php.net/supported-versions.php
不受支持分支列表:https://www.php.net/eol.php
升级并非只修改 Dockerfile 的 PHP 标签即可,还需要核查旧 Laravel 版本、Composer 依赖、扩展以及代码中的弃用接口。
12.2 Meilisearch 版本偏旧¶
当前仓库 compose.yml 使用 Meilisearch 0.24.0。阅读项目时应以这个版本和仓库锁定的客户端依赖为准,不要假设新版 Meilisearch 的每一项功能都能直接用于当前集成。
12.3 一个容器运行 Nginx 和 PHP-FPM 的管理问题¶
Docker 容器常见的最佳实践是让容器围绕一个主要服务运行;但在实际项目中,一个容器内运行多个进程也是可行的,只是要认真处理进程管理、退出信号、日志、健康检查和故障重启。
MeEdu 的 Dockerfile 使用 tini 作为入口初始化程序,然后运行初始化命令、Nginx 与 PHP-FPM。tini 可以帮助处理 PID 1 与子进程回收问题,但并不等于完整的多服务进程管理器。若要深入评估其可靠性,应检查启动命令返回码、Nginx / PHP-FPM 是否运行在适当模式,以及容器停止时是否能正确关闭各进程。
12.4 Compose 的启动顺序不等于服务已就绪¶
depends_on 主要表达服务依赖和启动顺序;若没有相应的健康检查与就绪逻辑,不能保证 MySQL / Redis 已经可以接受业务连接。固定 sleep 15 是一种简单等待方式,但在启动较慢的环境下可能不够稳健。
12.5 密钥、密码与端口¶
- 生产部署应为应用生成独立的
APP_KEY和JWT_SECRET。 - 数据库密码、Redis 密码和 Meilisearch 管理密钥不要使用公开默认值。
- Compose 示例没有把 MySQL、Redis、Meilisearch 的端口直接发布到宿主机,这有助于减少意外暴露;若要对外开放,应明确网络边界、防火墙和鉴权策略。
- 不要将拥有管理权限的 Meilisearch Key 直接下发到浏览器。
13. 推荐的源码阅读顺序¶
如果目标是理解 PHP 后端开发与部署,不建议从 public/index.php 一直逐行硬读。它主要是框架入口。更有价值的顺序如下:
- 先看整体构建:
Dockerfile和compose.yml,了解镜像是如何构建、服务如何运行。 - 再看 Web 请求入口:
docker/nginx/default.conf与xyz.meedu.api/public/index.php,理解 Nginx → PHP-FPM → Laravel 的请求路径。 - 看路由如何映射 Controller:
RouteServiceProvider.php、routes/frontend-v2.php、routes/backend-v1.php。 - 从一个真实功能追踪业务:例如
routes/frontend-v2.php中/courses或/course/{id},追踪对应 Controller,再看 Service 和 DAO / Model。 - 学习搜索集成:
config/scout.php、SearchRecord.php、FullSearchService.php、OtherDao.php。 - 最后看运行参数:
docker/php/php-fpm.d/www.conf,理解工作进程数量、空闲进程以及pm.max_requests等参数。
推荐路径可以总结为:
Dockerfile / compose.yml
↓
Nginx 配置
↓
public/index.php
↓
RouteServiceProvider + routes/*.php
↓
Controller
↓
Service
↓
DAO / Model
↓
MySQL / Redis / Meilisearch
14. 主要参考链接¶
MeEdu 源码¶
- 项目首页:https://github.com/Qsnh/meedu
- README:https://github.com/Qsnh/meedu/blob/main/README.md
- Dockerfile:https://github.com/Qsnh/meedu/blob/main/Dockerfile
- Docker Compose:https://github.com/Qsnh/meedu/blob/main/compose.yml
- Nginx 配置:https://github.com/Qsnh/meedu/blob/main/docker/nginx/default.conf
- PHP-FPM pool 配置:https://github.com/Qsnh/meedu/blob/main/docker/php/php-fpm.d/www.conf
- Laravel 入口:https://github.com/Qsnh/meedu/blob/main/xyz.meedu.api/public/index.php
- RouteServiceProvider:https://github.com/Qsnh/meedu/blob/main/xyz.meedu.api/app/Providers/RouteServiceProvider.php
- Laravel Scout 配置:https://github.com/Qsnh/meedu/blob/main/xyz.meedu.api/config/scout.php
- SearchRecord 模型:https://github.com/Qsnh/meedu/blob/main/xyz.meedu.api/app/Meedu/ServiceV2/Models/SearchRecord.php
- FullSearchService:https://github.com/Qsnh/meedu/blob/main/xyz.meedu.api/app/Meedu/ServiceV2/Services/FullSearchService.php
- API README:https://github.com/Qsnh/meedu/blob/main/xyz.meedu.api/README.md
官方资料¶
- PHP-FPM:https://www.php.net/manual/en/install.fpm.php
- PHP 支持版本:https://www.php.net/supported-versions.php
- Laravel Scout:https://laravel.com/docs/8.x/scout
- Meilisearch 文档:https://www.meilisearch.com/docs
- Docker 多阶段构建:https://docs.docker.com/build/building/multi-stage/
- Nginx FastCGI 模块:https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html
一句话总结¶
MeEdu 是一个传统但完整的前后端分离 Web 应用案例:前端用 Node.js 工具链构建为静态文件,Nginx 对外处理 HTTP;PHP-FPM 通过 FastCGI 执行 Laravel 后端;Laravel 的路由把干净的 URL 映射到业务控制器;MySQL 保存业务数据,Redis 提供缓存等能力,Meilisearch 负责专门的全文搜索。Docker 多阶段构建只是在构建期间使用多个基础环境,最后仍然可以形成一个包含多个组件的应用镜像,再由 Docker Compose 与数据库、缓存、搜索服务一起运行。