如何正确地将 Laravel 应用部署在子目录下?
先让 Laravel 识别挂载前缀,再配置前端资源路径
Laravel 可以通过反向代理部署在 /demo/ 下。题目中,代理剥离 /demo,后端从 / 提供服务,可以按以下方式配置:
- Nginx 传递
X-Forwarded-Prefix: /demo。 - Laravel 信任实际的前置代理及该请求头。
APP_URL设置为完整的外部应用地址。- Vite 单独配置构建资源的公开地址。
- Ziggy、Inertia 使用 Laravel 已解析的请求 URL。
urlResolver() 是 Inertia 提供的扩展接口,使用它本身不算临时补丁。如果 Laravel 已正确识别前缀,所用 Inertia 适配器又默认通过 $request->fullUrl() 生成页面 URL,通常就不需要额外拼接 /demo。
运行时请求地址、构建资源地址和前端写死的路径需要分别处理,没有一个环境变量能自动修改整个工具链中的所有 URL。
1. 保持代理剥离前缀的规则一致
当前配置中,proxy_pass 末尾的斜杠会影响转发路径:
location ^~ /demo/ {
proxy_pass http://demo_app/;
}
这里的 proxy_pass 包含 URI /,Nginx 会用它替换匹配到的 /demo/:
外部请求:/demo/users?page=2
后端收到:/users?page=2
按 Nginx 的 URI 替换规则,不能随意去掉 proxy_pass 末尾的 /。Nginx 文档
可以保留现有结构,在已有的 HTTPS server 块中加入以下配置:
location = /demo {
return 301 /demo/$is_args$args;
}
location ^~ /demo/ {
proxy_pass http://demo_app/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Prefix /demo;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
这份配置假设 Nginx 直接接收公网 HTTPS 请求。如果前面还有负载均衡器或 TLS 终止代理,$scheme、$server_port 可能只反映内部连接的协议和端口,需要按实际代理链传递外部信息。
后端 Web 服务器仍应以 Laravel 的 public 目录为站点根目录,并将动态请求交给 public/index.php。
2. 配置 Laravel 的可信代理和请求头
Laravel 的请求对象基于 Symfony HttpFoundation。可信的 X-Forwarded-Prefix 会参与 getBaseUrl() 的计算,影响应用生成的 URL。Symfony 子路径代理说明
沿用题目中的中间件结构,可以这样配置:
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
protected $proxies = [
'10.0.0.10',
];
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_PREFIX;
}
将示例中的 10.0.0.10 替换为应用实际看到的代理来源 IP。这个地址可能是前置 Nginx 的内网地址,也可能经过容器网络或 NAT 转换,不能只根据域名解析结果判断。
还需要确认以下配置:
- 该中间件已注册,并在依赖请求 URL 的中间件之前执行。
- 中间代理没有丢弃或覆盖
X-Forwarded-Prefix。 - 后端只允许预期的代理访问,前置代理会覆盖客户端传入的同名头。
如果项目在 bootstrap/app.php 中配置中间件,应通过 withMiddleware() 内的 trustProxies(at: ..., headers: ...) 设置对应值。只创建 App\Http\Middleware\TrustProxies 类而不注册,不会生效。Laravel 可信代理配置
题目中的 HEADER_X_FORWARDED_AWS_ELB 是一组请求头标志。这里使用普通 Nginx 代理,显式列出上述标志即可,不必再加入它。
3. 用请求结果定位问题
调整 Ziggy 或 Inertia 前,先检查 Laravel 的请求对象。
假设浏览器访问:
https://my-domain.com/demo/users?page=2
后端应用在内部从 / 提供服务。代理配置生效后,经过可信代理中间件的 HTTP 请求应得到以下结果:
| 表达式 | 预期结果 |
|---|---|
$request->getBaseUrl() | /demo |
$request->getPathInfo() | /users |
$request->root() | https://my-domain.com/demo |
$request->fullUrl() | https://my-domain.com/demo/users?page=2 |
url('/users') | https://my-domain.com/demo/users |
请在实际环境中核对这些值。Laravel 的 root() 会组合外部协议、主机和 getBaseUrl()。Laravel Request 实现
如果 getBaseUrl() 仍为空,优先检查可信代理配置和请求头传递。此时手动给 Inertia 补前缀,只能修正 Inertia 的页面 URL,其他地方生成的 URL 仍可能出错。
在这种代理结构下,Laravel 内部路由继续定义为 /users,无需统一添加 Route::prefix('demo'),否则会混淆部署前缀与业务路由。
4. 配置 APP_URL 和 Ziggy
将 APP_URL 设置为完整的外部应用地址:
APP_URL=https://my-domain.com/demo
HTTP 请求期间,Laravel 通常根据当前请求生成 URL,APP_URL 无法代替可信代理配置。命令行环境中,Laravel 会使用 config('app.url') 创建请求上下文,因此队列和命令行任务生成链接时也需要这个值。Laravel 命令行请求初始化实现
Ziggy 2.x 默认通过 Laravel 的 url('/') 获取基础地址。确认请求解析正确后,可以先移除手动覆盖的 'url':
use Tighten\Ziggy\Ziggy;
'ziggy' => fn () => [
...(new Ziggy)->toArray(),
'location' => $request->fullUrl(),
],
这样,Ziggy 的基础地址就与 Laravel 的 URL 生成器保持一致,fullUrl() 也会保留当前位置的查询参数。Ziggy 实现
如果前端使用 php artisan ziggy:generate 生成的静态配置文件,需要在正确的部署环境下重新生成。后端配置改变后,已经打包进 JavaScript 的旧地址不会自动更新。
5. Inertia 是否需要 urlResolver()
是否需要自定义解析器,取决于项目安装的 inertiajs/inertia-laravel 版本及其实现。
例如,inertia-laravel v2.0.0 默认使用 $request->fullUrl(),再移除协议和主机部分。只要 Laravel 已识别 /demo,页面对象中的 url 就应包含该前缀。Inertia v2.0.0 实现
可以先移除自定义解析器,检查 Inertia 响应中的 url 是否如下:
{
"url": "/demo/users?page=2"
}
如果当前版本或项目定制仍需要解析器,可以通过请求对象获取经过可信代理处理的前缀:
use Illuminate\Http\Request;
public function urlResolver()
{
return function (Request $request): string {
$url = $request->getBaseUrl() . $request->getPathInfo();
$query = $request->getQueryString();
return $query === null ? $url : $url . '?' . $query;
};
}
这个示例适用于支持中间件 urlResolver() 的适配器版本,使用时应核对项目安装版本的接口。Inertia 中间件实现
这里使用请求对象解析后的结果。直接调用 $request->headers->get('X-Forwarded-Prefix') 读取原始头,不会自动应用可信代理判断,容易让局部代码与框架的 URL 处理规则不一致。
Inertia 页面对象的 url 与 HTTP 重定向的 Location 也需要分别检查,修改解析器不会修复所有跳转目标。
6. 配置 Vite 的资源公开地址
VITE_BASE_PATH 不是 Vite 或 Laravel Vite 插件自动识别的标准配置项,只有项目代码显式读取并使用它时才会生效。
生产部署如果使用官方 Laravel Vite 插件,并默认输出到 public/build,可以设置:
APP_URL=https://my-domain.com/demo
ASSET_URL=https://my-domain.com/demo
Laravel Vite 插件会根据 ASSET_URL 和构建目录计算构建时的 base。默认配置下,资源地址应类似:
https://my-domain.com/demo/build/assets/app-<hash>.js
这里的 ASSET_URL 不要包含 /build,插件会追加构建目录。如果 vite.config.js 已显式设置 base,则会优先使用该值,需要检查它是否仍指向 /build/ 或其他旧路径。Laravel Vite 文档、Laravel Vite 插件实现
按项目的部署流程重新构建并更新配置缓存,例如:
php artisan config:clear
npm run build
php artisan config:cache
构建环境也必须提供正确的 ASSET_URL。只修改生产服务器的 .env,不会改变已经构建好的 JavaScript 和 CSS。
Blade 中继续使用项目原有的 @vite(...),并确认生产环境没有残留指向开发服务器的 public/hot 文件。
7. 检查前端路径和完整跳转流程
即使以上配置正确,下面的写法仍会让浏览器访问域名根目录下的 /users:
router.visit('/users');
它访问的是 https://my-domain.com/users。使用 Ziggy 时,应通过命名路由生成目标地址:
router.visit(route('users.index'));
这里假设项目已定义 users.index 路由。模板中的链接、表单提交地址,以及写死的 /images/...、/storage/... 等资源路径,也需要逐一检查。
部署完成后,至少验证以下行为:
- 直接打开
/demo/和一个深层页面,刷新后仍能访问。 - Inertia 导航及带查询参数的分页保留
/demo。 - 登录、退出、验证失败后的跳转目标正确。
- CSS、JavaScript、动态导入及字体资源从预期路径加载。
- 邮件或队列任务生成的链接包含
/demo。
如果根站点也有登录系统,还应检查 Session Cookie 名称和路径,避免同域应用互相覆盖 Cookie。第三方包自行生成的回调地址或资源地址,需要按该包的配置方式处理。
备注:内容仅供参考。
