PAYATHON 2026

如何正确地将 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。第三方包自行生成的回调地址或资源地址,需要按该包的配置方式处理。

备注:内容仅供参考。