ファランクスブログ

© 2026 all rights reserved.
  1. blog
  2. r2-storage
  • 環境変数
  • R2ダッシュボード
  • CORS設定
  • カスタムドメイン
  • 1対1の関係
  • 画像のupload
  • Vercel:画像サイズ

Cloudflare:R2

2026年2月21日
  • 無料の容量が多い:読み取り/書き込み

  • 人気ストレージである「Amazon-S3」と操作が近い

npm i @aws-sdk/client-s3 uuid
npm i sharp

環境変数

R2_ACCESS_KEY_ID="" 
R2_SECRET_ACCESS_KEY=""

R2_BUCKET_NAME=""

R2_ENDPOINT=""
R2_ACCOUNT_ID=""
R2_PUBLIC_DOMAIN="" 
  • 上2つ:(ダッシュボード/ R2 オブジェクト ストレージ / APIトークンの管理 / アカウント API トークンの作成)

    • APIトークンを作成する時に確認できる。*作成後は確認できない

  • 真ん中:バケット名

  • 下3つ:( ダッシュボード/ R2 オブジェクト ストレージ / 設定 )

    • R2_ENDPOINT: https://{ R2_ACCOUNT_ID }.r2.cloudflarestorage.com*バケット名は含まない

    • R2_ACCOUNT_ID: { R2_ACCOUNT_ID }

    • R2_PUBLIC_DOMAIN:

      • 開発:「パブリック開発URL」を有効にすると表示されるもの( https://pub-xxx.r2.dev )

      • 本番:「カスタムドメイン」から追加するもの(例:cdn.xxx.com)

R2ダッシュボード

CORS設定

  1. 開発用に http://localhost:3000/ を追加

  2. 本番用にドメインを追加する際は 以下のJSON形式で入力

  {
    "AllowedOrigins": [
      "本番ドメイン"
    ],
    "AllowedMethods": [
      "GET",
      "POST",
      "DELETE"
    ]
  }

カスタムドメイン

cloudflareにドメインを登録しているならすぐにセットできる

  1. cdn.xxx.com としてカスタムドメインを追加すると、R2用のDNSレコードが作成される

    1. xxx.com = 登録しているドメイン

  2. 環境変数:R2_PUBLIC_DOMAIN

    1. .env.development: パブリック開発URL

    2. .env.production :作成した cdn.xxx.com

1対1の関係

バケットはアプリ毎に作成し、1つのR2用カスタムドメイン(cdn.example.com )を使いまわすことは出来ない。「既に紐付いているバケットがある」旨のメッセージが出て作成できない。

// X
cdn.example.com → bucket1
cdn.example.com → bucket2
  • 素直にプロジェクト毎にカスタムドメインを増やしアプリ・カスタムドメイン・バケットは1対1で管理した方が良い。Cloudflareでドメインを管理しているならカスタムドメイン( cdn2.example.com)の追加時に自動で作成してくれる。

// O
cdn1.example.com → bucket1
cdn2.example.com → bucket2

*非推奨:

  • 1つのカスタムドメイン、バケットでも複数のアプリで運用はできる。

  • やり方は1つのバケットに階層を設ける、つまり画像をバケットに保存するときに /web のようにプロジェクト毎にユニークな接頭辞をつけて保存する。

  • ややハックっぽい感じがするのでドメイン数の制限?みたいなものに引っかかったらこのやり方を使う。

cdn.example.com/blog/xxx.webp
cdn.example.com/web/yyy.webp

画像のupload

r2/client.ts:

import { S3Client } from "@aws-sdk/client-s3";

export const s3 = new S3Client({
  region: "auto",
  endpoint: process.env.R2_ENDPOINT,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID!,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
  },
});

action:

  if (file) {
    const ext = "webp";
    const key = `folder-image-${folderId}.${ext}`;

    const arrayBuffer = await file.arrayBuffer();
    const buffer = Buffer.from(arrayBuffer);
    const webpBuffer = await sharp(buffer).webp().toBuffer();

    await s3.send(
      new PutObjectCommand({
        Bucket: process.env.R2_BUCKET_NAME!,
        Key: key,
        Body: webpBuffer,
        ContentType: "image/webp",
      })
    );

    iconUrl = `${process.env.R2_PUBLIC_DOMAIN}/${key}`;
  }
  • inputタグで取得したfileを受取りR2にuploadする

  • webpはpngの1/10くらいのサイズなので、webpに変換する

Vercel:画像サイズ

1MB以上の画像をserverActionで処理しようとすると画像のUploadが出来ない。Vercel-Logを見るとエラーが出ている。

Uncaught Exception: Error: Body exceeded 1 MB limit.

クライアント側でfile.sizeが1MBを超えたらtoastを出すようにする

  const MAX_IMAGE_SIZE = 900000;

  const handleUpload = async () => {
    if (!file) return;

    if (file.size > MAX_IMAGE_SIZE) {
      toast.error("Image size must be less than 1MB.", {
        style: {
          background: "red",
          color: "#fff",
        },
      });
      return;
    }
    setLoading(true);
    try {
      await uploadImage(id, file);
      setFile(null);
    } finally {
      setLoading(false);
    }
  };
  • 1MBは1,000,000 bytesなので少し余裕をもたせる

cloud
/
cloudflare
  • 環境変数
  • R2ダッシュボード
  • CORS設定
  • カスタムドメイン
  • 1対1の関係
  • 画像のupload
  • Vercel:画像サイズ