跳转至

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 对比

目标是提供:

GET /api/courses

返回 JSON:

[
  {"id": 1, "name": "PHP 入门"},
  {"id": 2, "name": "Java 入门"}
]

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 的实际课程接口代码:

// routes/example.php(示意)
Route::get('/courses', [CourseController::class, 'index']);
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 程序可以通过以下方式获取信息:

  1. 环境变量:如请求方法、查询字符串、客户端 IP。
  2. 标准输入(STDIN):例如 POST 请求正文。
  3. 标准输出(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 中配置了:

listen = 127.0.0.1:9000

两者对应起来了: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 结尾

例如:

GET /api/v2/courses
GET /api/v2/course/123

这些是普通 HTTP API 路径,不需要带 .php。Nginx 中对 .php 的规则是为了将 PHP 脚本交给 PHP-FPM;它不表示只有 .php 结尾的 URL 才是 API。

6.2 为什么 /api/v2/courses 最终会执行 index.php

MeEdu Nginx 的 API 服务配置中包含:

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

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 对象,并用它们匹配路由。

假设客户端发送:

GET /api/v2/courses?page=1 HTTP/1.1
Host: meedu.example.com
Accept: application/json

这条请求至少包含以下信息:

信息 示例值
HTTP 方法 GET
请求 URI /api/v2/courses?page=1
查询字符串 page=1
入口 PHP 文件 public/index.php

第一步:Nginx 改变执行目标,不是抹掉原始请求

API Nginx 配置中的规则:

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

会先检查请求是否对应实际存在的文件或目录;如果不存在,就内部转到 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 = Illuminate\Http\Request::capture();
$response = $kernel->handle($request);

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 源码中的实际控制器名称:

Route::get('/courses', [CourseController::class, 'index']);

如果这条路由注册在 /api/v2 前缀的路由组中,那么对外完整路径就是:

GET /api/v2/courses

匹配成功后,Laravel 才会调用对应的控制器方法;控制器再执行业务逻辑并返回响应。

放回 MeEdu 项目中看

MeEdu 并不是简单地把所有 API 都写在默认的 routes/api.php 中。它通过 app/Providers/RouteServiceProvider.php 注册不同的路由文件,其中 routes/frontend-v2.php 对应前台 V2 API,路由组添加 /api/v2 前缀。因此,可以沿着以下路径查找真实接口:

  1. 查看 xyz.meedu.api/docker/nginx/default.conf(仓库中的对应路径是 docker/nginx/default.conf),确认 Nginx 的 try_files 和 FastCGI 配置。
  2. 查看 xyz.meedu.api/public/index.php,理解 Laravel 的入口。
  3. 查看 xyz.meedu.api/app/Providers/RouteServiceProvider.php,确认路由文件注册和前缀。
  4. 查看 xyz.meedu.api/routes/frontend-v2.php,找到 /courses 对应的路由定义。
  5. 沿着路由指向的控制器继续阅读业务代码。

也可以在项目后端目录执行:

php artisan route:list

如果当前 Laravel Artisan 版本支持路径筛选,也可以尝试:

php artisan route:list --path=courses

这样可以核对实际注册的 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 查询,例如:

SELECT *
FROM courses
WHERE title LIKE '%Python%';

但对于跨多个文本字段检索、相关性排序、拼写容错、搜索体验优化等需求,专门的搜索引擎通常更合适。是否需要独立搜索引擎仍取决于数据规模与业务需要,小项目并非一开始就必须部署它。

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_HOST=http://meilisearch:7700
MEILISEARCH_KEY=

meilisearch 是 Compose 网络内的服务名,7700 是该服务使用的端口。这个地址一般是容器之间访问的地址,不等同于公开给浏览器访问的地址。

7.4 Laravel Scout 的作用

MeEdu 使用 Laravel Scout 以及 PHP 客户端 meilisearch/meilisearch-php 来集成搜索。

xyz.meedu.api/config/scout.php 中配置了:

'driver' => 'meilisearch',
'prefix' => 'meedu_',

并通过 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:指定搜索引擎驱动、索引前缀和连接配置。

代码调用形态类似:

SearchRecord::search($keywords)
    ->take($limit)
    ->get();

使用 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 网络中。应用容器可以通过服务名访问其他容器,例如:

DB_HOST=mysql
REDIS_HOST=redis
MEILISEARCH_HOST=http://meilisearch:7700

这里使用的是 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。

所以,这个项目里同时存在两种不同通信:

  1. 浏览器 → Nginx:HTTP / HTTPS;
  2. 前端站点 Nginx → API Nginx:HTTP;
  3. API Nginx → PHP-FPM:FastCGI。

不要把 HTTP 反向代理与 FastCGI 混为一谈。


11. 与 Java / Spring Boot 部署方式的对比

11.1 典型请求链路

MeEdu:

浏览器 --HTTP--> Nginx --FastCGI--> PHP-FPM --> Laravel

常见 Spring Boot:

浏览器 --HTTP--> 内嵌 Tomcat / Spring Boot --> Controller --> Service

也可以在 Java 系统前面放 Nginx:

浏览器 --HTTP--> Nginx --HTTP 反向代理--> Spring Boot :8080

因此,不应简单地认为 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 一直逐行硬读。它主要是框架入口。更有价值的顺序如下:

  1. 先看整体构建:Dockerfile 和 compose.yml,了解镜像是如何构建、服务如何运行。
  2. 再看 Web 请求入口:docker/nginx/default.conf 与 xyz.meedu.api/public/index.php,理解 Nginx → PHP-FPM → Laravel 的请求路径。
  3. 看路由如何映射 Controller:RouteServiceProvider.php、routes/frontend-v2.php、routes/backend-v1.php。
  4. 从一个真实功能追踪业务:例如 routes/frontend-v2.php 中 /courses 或 /course/{id},追踪对应 Controller,再看 Service 和 DAO / Model。
  5. 学习搜索集成:config/scout.php、SearchRecord.php、FullSearchService.php、OtherDao.php。
  6. 最后看运行参数: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 源码

官方资料


一句话总结

MeEdu 是一个传统但完整的前后端分离 Web 应用案例:前端用 Node.js 工具链构建为静态文件,Nginx 对外处理 HTTP;PHP-FPM 通过 FastCGI 执行 Laravel 后端;Laravel 的路由把干净的 URL 映射到业务控制器;MySQL 保存业务数据,Redis 提供缓存等能力,Meilisearch 负责专门的全文搜索。Docker 多阶段构建只是在构建期间使用多个基础环境,最后仍然可以形成一个包含多个组件的应用镜像,再由 Docker Compose 与数据库、缓存、搜索服务一起运行。