ファイルストレージ

プラットフォームの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が行うのは次の作業です:

  1. ヘルパーをコピー: apps/example-app/app/_lib/storage.tsapps/<your-app>/app/_lib/storage.ts へ。
  2. 依存関係を追加: アプリの package.json に Example App と同じバージョンで追加し、モノレポルートで pnpm install:
package.jsonjson
{
"dependencies": {
  "@aws-sdk/client-s3": "^3.1079.0",
  "@aws-sdk/s3-request-presigner": "^3.1079.0"
}
}
  1. 5つの値を対応付け: apps/<your-app>/deploy.config.yml に書きます。すでに secrets: ブロックがあれば、その中に行を足してください:
deploy.config.ymlyaml
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"
  1. secret を転送: deploy-app-<your-app>-staging.ymldeploy-app-<your-app>-production.yml の両方の secrets: ブロックに、Example App のワークフローと同じく BUCKET_SECRET_ACCESS_KEY を追加します。
  2. PRを作成: コードがストレージを使うのに対応付けがない、対応付けがあるのに使うコードがない、デプロイワークフローが secret を転送していない、のいずれかでPRチェックが失敗します。

ヘルパーの使い方

サーバーアクションやサーバーコンポーネントでtypescript
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 を返すので、ページを落とさずに説明を表示できます。ほかに listFilesfileExistsdeleteFile があり、Example App のギャラリーはすべて使っています。

知っておきたいルール

  • キーは必ずアプリ名で始めるmy-app/...)。1つのバケットを全アプリで共有しており、分けているのはこのプレフィックスだけです。
  • ブラウザから来たキーやファイルを信用しない。 バケットに触る前に、アプリが使うパターンでキーを確認し、ファイルは先頭のバイトを読んで中身を確かめます。Example App の gallery.ts が両方行っています。
  • キーに個人情報を入れない。 キーはサーバーで作り、アップロードされたファイル名やメールアドレスは使いません。
  • 画像は <img> で表示するnext/image ではなく)。署名付きリンクは期限切れになり、next/image はサーバー側で取得してしまいます。
  • 署名付きリンクは、持っている人なら誰でも開けます(期限まで)。ページ上のリンクは短く(1時間)。最長は7日です。
  • 1MBを超えるアップロードには、Example App が5MB上限のために行っているように、next.config.tsserverActions.bodySizeLimit を引き上げます。

Railwayストレージバケットの作成

クライアントごとに1回行います。プラットフォームアプリは Railway環境ごとに1つのバケット を共有します(ステージングと本番は別インスタンス・別認証情報)。

1. Railwayでバケットを作成

2回行います — Railwayで staging を選択した状態で1回、production を選択した状態で1回。

  1. クライアントのRailwayプロジェクトを開く(例: shimomoto-vibe-coding-platform)。
  2. 環境ドロップダウンで staging(または production)に切り替える。
  3. プロジェクトキャンバスで CreateBucket
  4. リージョンを選択(後から変更不可)。表示名を設定(例: shimomoto-storage)。この表示名はラベルに過ぎません — S3 のバケット名は別で、後から railway bucket rename してもS3名は変わりません。BUCKET_NAME には必ず Credentials の値を使ってください。
  5. バケットのデプロイ完了を待つ。
  6. バケットを開き Credentials タブから以下を控える:
Railway Credentialsプラットフォーム環境変数備考
ENDPOINTBUCKET_ENDPOINT例: https://t3.storageapi.dev
ACCESS_KEY_IDBUCKET_ACCESS_KEY_IDS3アクセスキー
SECRET_ACCESS_KEYBUCKET_SECRET_ACCESS_KEYS3シークレットキー
BUCKETBUCKET_NAMES3 API用バケット名(ハッシュ付き)。RAILWAY_BUCKET_NAME ではない
REGIONBUCKET_REGION通常 auto

Railwayドキュメント: Storage Buckets

2. GitHub Environmentsに値を登録

クライアントリポジトリ(algoritmi-tech/<client>-vibe-coding-platform)で Settings → Environments を開く。

stagingproduction の両方に登録 — 値はそのRailway環境で作成したバケットの Credentials から:

GitHubキータイプ取得元
BUCKET_ENDPOINTVariableRailway ENDPOINT
BUCKET_ACCESS_KEY_IDVariableRailway ACCESS_KEY_ID
BUCKET_SECRET_ACCESS_KEYSecretRailway SECRET_ACCESS_KEY
BUCKET_NAMEVariableRailway BUCKET
BUCKET_REGIONVariableRailway 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 → サービス → VariablesBUCKET_* が5つ表示されることを確認します。

環境変数(ランタイム)

変数目的
BUCKET_ENDPOINTS3互換APIのエンドポイント
BUCKET_ACCESS_KEY_ID認証情報
BUCKET_SECRET_ACCESS_KEY認証情報
BUCKET_NAME読み書き対象のバケット
BUCKET_REGIONオプション。未設定時は auto

最初の4つのいずれかが欠けていると getStorage()null を返します。環境変数も参照。

バケット設定のトラブルシュート

症状対処
ページに「ストレージが設定されていない」と表示されるアプリの deploy.config.ymlBUCKET_* を5つとも対応付け、両方のデプロイワークフローが secret を転送しているか確認し、再デプロイ
PRプレビューでは動くが本番で動かない本番デプロイワークフローで BUCKET_SECRET_ACCESS_KEY を転送する
SignatureDoesNotMatchBUCKET_ENDPOINT とキーをバケットの Credentials タブと照合
NoSuchBucketBUCKET_NAME は表示名ではなく Credentials の BUCKET
ステージングのみ動作本番用バケット + GitHub production 用キーが別途必要

つまづきポイント(一度は読んでおく価値あり)

  • Railway固有の癖はヘルパーが処理済みです。 BUCKET_ENDPOINT の末尾スラッシュ(SignatureDoesNotMatch の原因)を除去し、パス形式でリクエストし、チェックサムヘッダは必要な操作でだけ送ります。自分でクライアントを書かず、そのままコピーしてください。
  • 対応付けだけでは足りません — 両方のデプロイワークフローで secret の転送も必要です。転送がないと、プレビュー(全 secret を受け取る)では動き、本番では動きません。
  • デプロイは変数を削除しません。 ストレージを使わなくなったら、対応付けと一緒に Railway 上の BUCKET_* も削除してください。
  • BUCKET_REGIONautoです。 実在のAWSリージョン名に置き換えないでください。

Claudeに聞く

アプリに写真のアップロードを追加
Claudeプロンプト
Example App のギャラリーと同じやり方で、meal-planner アプリに写真のアップロードを追加してください。apps/example-app/app/_lib/storage.ts をコピーし、@aws-sdk の依存関係を追加、deploy.config.yml に BUCKET_* の5つの値を対応付け、両方のデプロイワークフローで BUCKET_SECRET_ACCESS_KEY を転送してください。写真は「meal-planner/photos/」プレフィックスに保存し、本当に画像かを確認して、署名付きリンクで表示してください。

クイズ

Quiz

バケットに保存されたファイルをユーザーにダウンロードさせる正しい方法は?