railway-storage
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRailway S3-Compatible Storage
Railway S3兼容存储
Railway provides S3-compatible storage buckets that work with standard AWS SDK. However, there are critical differences from AWS S3.
Railway提供与标准AWS SDK兼容的存储桶,但与AWS S3存在一些关键差异。
Critical: Private Buckets Only
重点:仅支持私有存储桶
Railway buckets are private by default and do not support public buckets. The setting is ignored.
ACL: "public-read"To serve files publicly:
- Proxy endpoint (recommended) - API route that fetches from S3 and serves to client
- Presigned URLs - Generate time-limited signed URLs for direct access
Railway存储桶默认是私有的,不支持公共存储桶。 设置会被忽略。
ACL: "public-read"要公开提供文件服务:
- 代理端点(推荐)- 从S3获取文件并提供给客户端的API路由
- 预签名URL - 生成限时的签名URL以实现直接访问
Environment Variables
环境变量
Railway auto-injects these when you link a storage bucket to your service. Use these exact names:
bash
AWS_ENDPOINT_URL=https://storage.railway.app
AWS_DEFAULT_REGION=auto
AWS_S3_BUCKET_NAME=your-bucket-name
AWS_ACCESS_KEY_ID=tid_xxx
AWS_SECRET_ACCESS_KEY=tsec_xxxImportant: Railway uses prefixed names by default. Do NOT use prefixes as they won't match Railway's injected variables.
AWS_*S3_*当你将存储桶链接到服务时,Railway会自动注入以下环境变量,请使用这些确切的名称:
bash
AWS_ENDPOINT_URL=https://storage.railway.app
AWS_DEFAULT_REGION=auto
AWS_S3_BUCKET_NAME=your-bucket-name
AWS_ACCESS_KEY_ID=tid_xxx
AWS_SECRET_ACCESS_KEY=tsec_xxx重要提示: Railway默认使用前缀的变量名。请勿使用前缀,因为它们与Railway注入的变量不匹配。
AWS_S3_S3 Client Setup
S3客户端配置
Use lazy initialization to avoid build-time errors (env vars unavailable during Docker builds):
typescript
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
let s3Client: S3Client | null = null;
function getS3Client(): S3Client {
if (!s3Client) {
s3Client = new S3Client({
endpoint: process.env.AWS_ENDPOINT_URL,
region: process.env.AWS_DEFAULT_REGION ?? "auto",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
forcePathStyle: true, // Required for Railway
});
}
return s3Client;
}Key points:
- is required
forcePathStyle: true - Never access at module level
process.env - Region is typically
"auto"
使用延迟初始化来避免构建时错误(Docker构建期间环境变量不可用):
typescript
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
let s3Client: S3Client | null = null;
function getS3Client(): S3Client {
if (!s3Client) {
s3Client = new S3Client({
endpoint: process.env.AWS_ENDPOINT_URL,
region: process.env.AWS_DEFAULT_REGION ?? "auto",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
forcePathStyle: true, // Required for Railway
});
}
return s3Client;
}关键点:
- 必须设置
forcePathStyle: true - 切勿在模块级别访问
process.env - 区域通常设置为
"auto"
Upload Implementation
上传实现
typescript
export async function uploadToS3(key: string, body: Buffer, contentType: string): Promise<string> {
const client = getS3Client();
const bucket = process.env.S3_BUCKET_NAME!;
await client.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: body,
ContentType: contentType,
// Note: ACL is ignored - Railway buckets are always private
}));
// Return proxy URL (not direct S3 URL)
return `/uploads/${key}`;
}typescript
export async function uploadToS3(key: string, body: Buffer, contentType: string): Promise<string> {
const client = getS3Client();
const bucket = process.env.S3_BUCKET_NAME!;
await client.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: body,
ContentType: contentType,
// 注意:ACL会被忽略 - Railway存储桶始终是私有的
}));
// 返回代理URL(而非直接S3 URL)
return `/uploads/${key}`;
}Proxy Endpoint Pattern
代理端点模式
Create an API route to serve files from S3:
typescript
// src/app/api/uploads/[...path]/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params;
const key = path.join("/");
const result = await getS3Object(key);
if (!result) {
return NextResponse.json({ error: "File not found" }, { status: 404 });
}
return new NextResponse(result.body, {
headers: {
"Content-Type": result.contentType,
"Content-Length": result.contentLength.toString(),
"Cache-Control": "public, max-age=31536000, immutable",
},
});
}Helper to read from S3:
typescript
export async function getS3Object(key: string) {
const client = getS3Client();
const response = await client.send(
new GetObjectCommand({ Bucket: process.env.AWS_S3_BUCKET_NAME!, Key: key })
);
if (!response.Body) return null;
return {
body: response.Body.transformToWebStream(),
contentType: response.ContentType || "application/octet-stream",
contentLength: response.ContentLength || 0,
};
}创建一个API路由来从S3提供文件服务:
typescript
// src/app/api/uploads/[...path]/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params;
const key = path.join("/");
const result = await getS3Object(key);
if (!result) {
return NextResponse.json({ error: "文件未找到" }, { status: 404 });
}
return new NextResponse(result.body, {
headers: {
"Content-Type": result.contentType,
"Content-Length": result.contentLength.toString(),
"Cache-Control": "public, max-age=31536000, immutable",
},
});
}从S3读取文件的工具函数:
typescript
export async function getS3Object(key: string) {
const client = getS3Client();
const response = await client.send(
new GetObjectCommand({ Bucket: process.env.AWS_S3_BUCKET_NAME!, Key: key })
);
if (!response.Body) return null;
return {
body: response.Body.transformToWebStream(),
contentType: response.ContentType || "application/octet-stream",
contentLength: response.ContentLength || 0,
};
}Next.js Rewrite Rule
Next.js 重写规则
typescript
// next.config.ts
const nextConfig: NextConfig = {
async rewrites() {
return [{ source: "/uploads/:path*", destination: "/api/uploads/:path*" }];
},
};typescript
// next.config.ts
const nextConfig: NextConfig = {
async rewrites() {
return [{ source: "/uploads/:path*", destination: "/api/uploads/:path*" }];
},
};Dependencies
依赖项
bash
bun add @aws-sdk/client-s3 @aws-sdk/s3-request-presignerbash
bun add @aws-sdk/client-s3 @aws-sdk/s3-request-presignerCommon Issues
常见问题
| Issue | Cause | Solution |
|---|---|---|
| Files upload but return 403 | Railway ignores ACL | Use proxy endpoint |
| Build fails with missing env vars | S3 client at module level | Use lazy initialization |
| "Invalid endpoint" error | Missing forcePathStyle | Add |
| Images don't update after upload | Browser/React Query caching | Add |
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 文件上传成功但返回403 | Railway忽略ACL设置 | 使用代理端点 |
| 构建失败,提示缺少环境变量 | S3客户端在模块级别初始化 | 使用延迟初始化 |
| "无效端点"错误 | 未设置forcePathStyle | 添加 |
| 上传后图片未更新 | 浏览器/React Query缓存 | 调用 |
URL Format
URL格式
Store proxy URLs in database, not direct S3 URLs:
- Correct:
/uploads/{teamId}/avatar/{filename} - Wrong:
https://storage.railway.app/bucket/{key}
在数据库中存储代理URL,而非直接S3 URL:
- 正确格式:
/uploads/{teamId}/avatar/{filename} - 错误格式:
https://storage.railway.app/bucket/{key}