ファイルストレージ
プラットフォームのS3互換Railwayバケットを使って、アプリでファイルを保存・表示する。
アップロードされたファイル(写真、PDF、エクスポート)をアプリで保存するときは、プラットフォームのRailwayストレージバケットを使います。ファイルは非公開で、ブラウザには署名付きURL(しばらくすると使えなくなる、ファイル1つへのリンク)を渡します。
ストレージはアプリごとに有効にします。バケットの鍵は、deploy.config.yml で要求したアプリにだけ届きます。ファイルを保存しないアプリが、他のアプリのファイルまで読める鍵を持つことはありません。
まず動くものを見る
Example App が手本です。/gallery を開くと、画像のアップロード、全員のアップロードの一覧、削除、画像へのリンクのメール送信を試せます。このページの内容はすべてそのコードで実装済みなので、そこからコピーしてください:
| ファイル | 内容 |
|---|---|
apps/example-app/app/_lib/storage.ts | ストレージのヘルパー。変更せずにコピー |
apps/example-app/app/_lib/gallery.ts | バケット内の置き場所、キーの確認、本当に画像かどうかの確認 |
apps/example-app/app/gallery/actions.ts | アップロード・削除・メール送信のサーバーアクション |
apps/example-app/deploy.config.yml | アプリにバケットの鍵を渡す5行 |
自分のアプリでストレージを有効にする
いちばん早いのはClaudeに頼むことです(下のプロンプト参照)。Claudeが行うのは次の作業です:
- ヘルパーをコピー:
apps/example-app/app/_lib/storage.tsをapps/<your-app>/app/_lib/storage.tsへ。 - 依存関係を追加: アプリの
package.jsonに Example App と同じバージョンで追加し、モノレポルートでpnpm install:
{
"dependencies": {
"@aws-sdk/client-s3": "^3.1079.0",
"@aws-sdk/s3-request-presigner": "^3.1079.0"
}
}- 5つの値を対応付け:
apps/<your-app>/deploy.config.ymlに書きます。すでにsecrets:ブロックがあれば、その中に行を足してください:
secrets:
BUCKET_ENDPOINT: "BUCKET_ENDPOINT"
BUCKET_ACCESS_KEY_ID: "BUCKET_ACCESS_KEY_ID"
BUCKET_SECRET_ACCESS_KEY: "BUCKET_SECRET_ACCESS_KEY"
BUCKET_NAME: "BUCKET_NAME"
BUCKET_REGION: "BUCKET_REGION"- secret を転送:
deploy-app-<your-app>-staging.ymlとdeploy-app-<your-app>-production.ymlの両方のsecrets:ブロックに、Example App のワークフローと同じくBUCKET_SECRET_ACCESS_KEYを追加します。 - PRを作成: コードがストレージを使うのに対応付けがない、対応付けがあるのに使うコードがない、デプロイワークフローが secret を転送していない、のいずれかでPRチェックが失敗します。
ヘルパーの使い方
import { getDownloadUrl, getStorage, uploadFile } from '@/app/_lib/storage';
const storage = getStorage();
if (!storage) {
// この環境にはバケットがない: その旨を表示し、ページは動かし続ける。
return;
}
// キーはアプリ名で始める。
await uploadFile(storage, 'my-app/receipts/0001.jpg', bytes, 'image/jpeg');
// ブラウザが1時間開けるリンク。
const url = await getDownloadUrl(storage, 'my-app/receipts/0001.jpg', 60 * 60);バケットが未設定なら getStorage() は null を返すので、ページを落とさずに説明を表示できます。ほかに listFiles、fileExists、deleteFile があり、Example App のギャラリーはすべて使っています。
知っておきたいルール
- キーは必ずアプリ名で始める(
my-app/...)。1つのバケットを全アプリで共有しており、分けているのはこのプレフィックスだけです。 - ブラウザから来たキーやファイルを信用しない。 バケットに触る前に、アプリが使うパターンでキーを確認し、ファイルは先頭のバイトを読んで中身を確かめます。Example App の
gallery.tsが両方行っています。 - キーに個人情報を入れない。 キーはサーバーで作り、アップロードされたファイル名やメールアドレスは使いません。
- 画像は
<img>で表示する(next/imageではなく)。署名付きリンクは期限切れになり、next/imageはサーバー側で取得してしまいます。 - 署名付きリンクは、持っている人なら誰でも開けます(期限まで)。ページ上のリンクは短く(1時間)。最長は7日です。
- 1MBを超えるアップロードには、Example App が5MB上限のために行っているように、
next.config.tsのserverActions.bodySizeLimitを引き上げます。
Railwayストレージバケットの作成
クライアントごとに1回行います。プラットフォームアプリは Railway環境ごとに1つのバケット を共有します(ステージングと本番は別インスタンス・別認証情報)。
1. Railwayでバケットを作成
2回行います — Railwayで staging を選択した状態で1回、production を選択した状態で1回。
- クライアントのRailwayプロジェクトを開く(例:
shimomoto-vibe-coding-platform)。 - 環境ドロップダウンで staging(または production)に切り替える。
- プロジェクトキャンバスで Create → Bucket。
- リージョンを選択(後から変更不可)。表示名を設定(例:
shimomoto-storage)。この表示名はラベルに過ぎません — S3 のバケット名は別で、後からrailway bucket renameしてもS3名は変わりません。BUCKET_NAMEには必ず Credentials の値を使ってください。 - バケットのデプロイ完了を待つ。
- バケットを開き Credentials タブから以下を控える:
| Railway Credentials | プラットフォーム環境変数 | 備考 |
|---|---|---|
ENDPOINT | BUCKET_ENDPOINT | 例: https://t3.storageapi.dev |
ACCESS_KEY_ID | BUCKET_ACCESS_KEY_ID | S3アクセスキー |
SECRET_ACCESS_KEY | BUCKET_SECRET_ACCESS_KEY | S3シークレットキー |
BUCKET | BUCKET_NAME | S3 API用バケット名(ハッシュ付き)。RAILWAY_BUCKET_NAME ではない |
REGION | BUCKET_REGION | 通常 auto |
Railwayドキュメント: Storage Buckets。
2. GitHub Environmentsに値を登録
クライアントリポジトリ(algoritmi-tech/<client>-vibe-coding-platform)で Settings → Environments を開く。
staging と production の両方に登録 — 値はそのRailway環境で作成したバケットの Credentials から:
| GitHubキー | タイプ | 取得元 |
|---|---|---|
BUCKET_ENDPOINT | Variable | Railway ENDPOINT |
BUCKET_ACCESS_KEY_ID | Variable | Railway ACCESS_KEY_ID |
BUCKET_SECRET_ACCESS_KEY | Secret | Railway SECRET_ACCESS_KEY |
BUCKET_NAME | Variable | Railway BUCKET |
BUCKET_REGION | Variable | Railway REGION |
BUCKET_SECRET_ACCESS_KEY だけが Secret です。残り4つは必ず Variable にしてください。
これは好みの問題ではなく動作要件です。GitHub の Variable はデプロイワークフローに自動で届きますが、Secret はアプリのワークフローが転送したときだけ届き、アプリが転送するのは BUCKET_SECRET_ACCESS_KEY だけです。BUCKET_ACCESS_KEY_ID を Secret にすると全アプリで空になり、getStorage() が null を返します。GitHub 側は正しく見えるため、原因が非常に分かりにくい失敗です。
3. ストレージを使うアプリを再デプロイ
CIはデプロイ時に値を書き込むので、GitHubに追加しただけでは、対応付けているアプリを再デプロイするまで何も変わりません。対象アプリは grep -l BUCKET_SECRET_ACCESS_KEY apps/*/deploy.config.yml で一覧でき、それぞれのデプロイワークフローを実行(または main に push)したら、Railway → サービス → Variables に BUCKET_* が5つ表示されることを確認します。
環境変数(ランタイム)
| 変数 | 目的 |
|---|---|
BUCKET_ENDPOINT | S3互換APIのエンドポイント |
BUCKET_ACCESS_KEY_ID | 認証情報 |
BUCKET_SECRET_ACCESS_KEY | 認証情報 |
BUCKET_NAME | 読み書き対象のバケット |
BUCKET_REGION | オプション。未設定時は auto |
最初の4つのいずれかが欠けていると getStorage() は null を返します。環境変数も参照。
バケット設定のトラブルシュート
| 症状 | 対処 |
|---|---|
| ページに「ストレージが設定されていない」と表示される | アプリの deploy.config.yml が BUCKET_* を5つとも対応付け、両方のデプロイワークフローが secret を転送しているか確認し、再デプロイ |
| PRプレビューでは動くが本番で動かない | 本番デプロイワークフローで BUCKET_SECRET_ACCESS_KEY を転送する |
SignatureDoesNotMatch | BUCKET_ENDPOINT とキーをバケットの Credentials タブと照合 |
NoSuchBucket | BUCKET_NAME は表示名ではなく Credentials の BUCKET |
| ステージングのみ動作 | 本番用バケット + GitHub production 用キーが別途必要 |
つまづきポイント(一度は読んでおく価値あり)
- Railway固有の癖はヘルパーが処理済みです。
BUCKET_ENDPOINTの末尾スラッシュ(SignatureDoesNotMatchの原因)を除去し、パス形式でリクエストし、チェックサムヘッダは必要な操作でだけ送ります。自分でクライアントを書かず、そのままコピーしてください。 - 対応付けだけでは足りません — 両方のデプロイワークフローで secret の転送も必要です。転送がないと、プレビュー(全 secret を受け取る)では動き、本番では動きません。
- デプロイは変数を削除しません。 ストレージを使わなくなったら、対応付けと一緒に Railway 上の
BUCKET_*も削除してください。 BUCKET_REGIONはautoです。 実在のAWSリージョン名に置き換えないでください。
Claudeに聞く
クイズ
バケットに保存されたファイルをユーザーにダウンロードさせる正しい方法は?