PAYATHON 2026

Express 静态资源在 Railway 返回 404,如何提前发现部署遗漏?

数据小陈

这次问题的直接原因是 public 没有进入部署容器。修正 .gitignore 后,还需要将资源加入 Git、提交,然后重新部署。为了提前发现类似遗漏,可以在 CI 中检查关键文件是否被 Git 跟踪,并在应用启动时验证部署环境中是否存在这些文件。

为什么本地正常,部署后却返回 404?

本地磁盘上有 public,Express 因此能读取其中的文件。被 .gitignore 忽略的未跟踪文件不会通过普通的 Git 提交流程进入仓库。如果部署使用的提交不包含这些文件,容器中的 Express 就找不到它们。

下面的配置将 public 设为静态根目录:

app.use(express.static(path.join(__dirname, 'public')));

文件与 URL 的对应关系如下:

文件位置请求 URL
public/main.css/main.css
public/js/home.js/js/home.js
public/images/fireplace3.jpg/images/fireplace3.jpg

原有的 EJS 引用符合这个映射,无需额外加上 /public。绝对路径只能帮助 Express 定位目录,无法补回部署时缺失的文件;调整 MIME type 或 Helmet CSP 也解决不了文件缺失的问题。

先确认资源确实进入了提交

在项目根目录检查忽略规则:

git check-ignore -v public/main.css public/js/home.js public/images/fireplace3.jpg

如果输出了匹配规则,可以据此修改 .gitignore。没有输出只说明这些路径没有匹配到可报告的忽略规则,不能说明文件已经提交;默认情况下,已跟踪的文件也不会被这条命令报告。

修正规则后,添加并检查资源:

git add public
git diff --cached --name-status
git ls-files --error-unmatch public/main.css public/js/home.js public/images/fireplace3.jpg

确认暂存内容正确后,将资源与 .gitignore 的修改一起提交,再部署包含该提交的版本。git ls-files 检查的是索引中的跟踪状态;如果只执行了 git add 而没有提交,远端部署仍然拿不到这些文件。

用 ES modules 明确静态目录的位置

ES modules 没有直接提供 CommonJS 的 __dirname。如果项目已经自行定义了它,可以继续使用原写法;也可以通过 import.meta.url 直接解析静态目录。

下面的示例假设入口文件 server.js 与 public 位于同一目录。启动时会检查三个关键资源,缺失时直接报错:

import express from 'express';
import { access } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';

const app = express();
const publicUrl = new URL('./public/', import.meta.url);
const publicDir = fileURLToPath(publicUrl);
const requiredAssets = [
  'main.css',
  'js/home.js',
  'images/fireplace3.jpg',
];

for (const asset of requiredAssets) {
  const assetPath = fileURLToPath(new URL(asset, publicUrl));

  try {
    await access(assetPath);
  } catch (cause) {
    throw new Error(`Required static asset is missing: ${assetPath}`, {
      cause,
    });
  }
}

app.use(express.static(publicDir));

// 在这里注册业务路由。

app.use((req, res) => {
  res.status(404).send('Not Found');
});

app.listen(process.env.PORT || 3000);

关键资源列表应根据项目实际情况维护。只检查 public 目录是否存在还不够,目录存在时,内部文件仍可能遗漏。

这个路径相对于入口模块的位置解析。如果实际入口是 src/server.js,而 public 位于项目根目录,就需要使用 ../public/。如果构建后入口变成 dist/server.js,也要根据部署产物中的目录结构重新确认路径。

把检查放进部署流程

对于题目中直接维护 CSS、JS 和图片的项目,可以采用以下流程:

  1. 将 public 作为源文件目录纳入 Git,移除错误的忽略规则。
  2. 在 CI 的仓库检出步骤之后,执行前面的 git ls-files --error-unmatch ...,让未跟踪的关键资源导致检查失败。
  3. 在最终运行环境中检查文件是否存在,捕获路径错误或资源未被复制的问题。
  4. 部署后请求 /main.css、/js/home.js 和一张关键图片,确认请求成功,且响应内容确实是对应资源。

Git 检查适合放在能够访问仓库元数据的 CI 阶段;启动检查则直接验证容器内的文件。两者检查的对象不同。

如果静态资源由构建工具生成,可以继续忽略生成目录,但部署构建必须生成这些文件,并将产物保留到最终运行环境。使用 Docker 或多阶段构建时,还需要检查 .dockerignore 和 COPY 是否遗漏了资源。

Railway 上需要专门的静态资源写法吗?

针对这次故障,普通的 express.static 配置就足够。可以继续使用相对于模块位置解析的目录,并将静态中间件放在最终的 404 处理器之前。

部署文件的约定需要明确:手工维护的资源随代码提交,构建生成的资源由构建流程产出。不要因为目录名是 public 就照搬其他框架的忽略规则。还要检查文件名大小写,确保 EJS 中的路径与实际文件完全一致。

备注:内容仅供参考。