このガイドでは、サンドボックスのディレクトリを R2 にスナップショットし、後から復元する方法を説明します。
サンドボックスがスリープしたあとも /workspace のようなプロジェクトディレクトリを戻したいときに、バックアップと復元を使います。永続ストレージを別パスにする場合は、代わりにバケットをマウントします。本番で /workspace にバケットをマウントすると、イメージがあらかじめ置いたファイルの上にマウントが重なります。
本番の復元がオーバーレイを使う理由は、ディレクトリバックアップ を参照してください。
-
R2 バケットを作成します。
npx wrangler r2 bucket create my-backup-bucket -
Wrangler 設定に
BACKUP_BUCKETR2 バインディングと署名付き URL の設定を追加します。{ "name": "my-sandbox-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-09-20", "compatibility_flags": ["nodejs_compat"], "containers": [ { "class_name": "Sandbox", "image": "./Dockerfile", }, ], "durable_objects": { "bindings": [ { "class_name": "Sandbox", "name": "Sandbox", }, ], ], "migrations": [ { "new_sqlite_classes": ["Sandbox"], "tag": "v1", }, ], "vars": { "BACKUP_BUCKET_NAME": "my-backup-bucket", "CLOUDFLARE_ACCOUNT_ID": "<YOUR_ACCOUNT_ID>", }, "r2_buckets": [ { "binding": "BACKUP_BUCKET", "bucket_name": "my-backup-bucket", }, ], }name = "my-sandbox-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-09-20" compatibility_flags = [ "nodejs_compat" ] [[containers]] class_name = "Sandbox" image = "./Dockerfile" [[durable_objects.bindings]] class_name = "Sandbox" name = "Sandbox" [[durable_objects.migrations]] new_sqlite_classes = [ "Sandbox" ] tag = "v1" [durable_objects.vars] BACKUP_BUCKET_NAME = "my-backup-bucket" CLOUDFLARE_ACCOUNT_ID = "<YOUR_ACCOUNT_ID>" [[durable_objects.r2_buckets]] binding = "BACKUP_BUCKET" bucket_name = "my-backup-bucket"バケットが管轄(jurisdiction)専用エンドポイントを使う場合は、
varsにBACKUP_BUCKET_ENDPOINTを追加します。EU バケットではhttps://<ACCOUNT_ID>.eu.r2.cloudflarestorage.comを使います。 -
R2 API の認証情報をシークレットとして保存します。
npx wrangler secret put R2_ACCESS_KEY_ID npx wrangler secret put R2_SECRET_ACCESS_KEYトークンは Cloudflare ダッシュボード ↗ の R2 > Overview > Manage R2 API Tokens で作成します。バックアップバケットに Object Read & Write を付与します。
import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const backup = await sandbox.createBackup({ dir: "/workspace" });import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const backup = await sandbox.createBackup({ dir: "/workspace" });ディレクトリは /workspace、/home、/tmp、/var/tmp、/app 配下の絶対パスである必要があります。
対象ディレクトリへ書き込むプロセスを停止してから、復元します。
import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const backup = await sandbox.createBackup({ dir: "/workspace" });
const result = await sandbox.restoreBackup(backup);import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const backup = await sandbox.createBackup({ dir: "/workspace" });
const result = await sandbox.restoreBackup(backup);本番では、復元はコピーオンライトのオーバーレイをマウントします。サンドボックスがスリープするかコンテナが再起動すると、マウントは失われます。保存したハンドルから再度復元してください。
復元先は backup.dir です。このフィールドは、当初バックアップしたディレクトリとは別の、許可されたディレクトリを指せます。
本番復元のあと、復元したツリー内でディレクトリ名を変更すると、EXDEV(cross-device link not permitted)で失敗することがあります。使い捨ての生成ディレクトリはバックアップから除外するか、復元後に削除します。Vite のキャッシュはその一例です。
const backup = await sandbox.createBackup({
dir: "/workspace/app",
excludes: ["node_modules/.vite"],
});const backup = await sandbox.createBackup({
dir: "/workspace/app",
excludes: ["node_modules/.vite"],
});await sandbox.restoreBackup(backup);
await sandbox.exec("rm -rf /workspace/app/node_modules/.vite");await sandbox.restoreBackup(backup);
await sandbox.exec("rm -rf /workspace/app/node_modules/.vite");この失敗は、アーカイブを展開する wrangler dev では起きません。オーバーレイ復元は ディレクトリバックアップ を参照してください。
git リポジトリで node_modules/ や dist/ のような .gitignore の一致をスキップするには、次のようにします。
const backup = await sandbox.createBackup({
dir: "/workspace",
gitignore: true,
});const backup = await sandbox.createBackup({
dir: "/workspace",
gitignore: true,
});ディレクトリが git リポジトリ内にない場合、gitignore は効果がありません。コンテナに git がインストールされていない場合、SDK は警告をログに出し、git ベースの除外なしで続行します。入れ子の .gitignore も適用されます。
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const checkpoint = await sandbox.createBackup({ dir: "/workspace" });
try {
await sandbox.exec("npm install some-experimental-package");
await sandbox.exec("npm run build");
} catch (error) {
await sandbox.restoreBackup(checkpoint);
}const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const checkpoint = await sandbox.createBackup({ dir: "/workspace" });
try {
await sandbox.exec("npm install some-experimental-package");
await sandbox.exec("npm run build");
} catch (error) {
await sandbox.restoreBackup(checkpoint);
}DirectoryBackup はシリアライズできます。KV、D1、または Durable Object のストレージに保存します。
const backup = await sandbox.createBackup({
dir: "/workspace",
name: "deploy-v2",
ttl: 604800, // 7 days
});
await env.KV.put(`backup:${userId}`, JSON.stringify(backup));
const stored = await env.KV.get(`backup:${userId}`);
if (stored) {
await sandbox.restoreBackup(JSON.parse(stored));
}const backup = await sandbox.createBackup({
dir: "/workspace",
name: "deploy-v2",
ttl: 604800, // 7 days
});
await env.KV.put(`backup:${userId}`, JSON.stringify(backup));
const stored = await env.KV.get(`backup:${userId}`);
if (stored) {
await sandbox.restoreBackup(JSON.parse(stored));
}名前は最大 256 文字です。デフォルトの TTL は 3 日(259200 秒)です。SDK は復元時に期限切れのバックアップを拒否します。R2 オブジェクトは削除しません。
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const shortBackup = await sandbox.createBackup({
dir: "/workspace",
ttl: 600, // 10 minutes
});
const longBackup = await sandbox.createBackup({
dir: "/workspace",
name: "daily-snapshot",
ttl: 604800, // 7 days
});const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const shortBackup = await sandbox.createBackup({
dir: "/workspace",
ttl: 600, // 10 minutes
});
const longBackup = await sandbox.createBackup({
dir: "/workspace",
name: "daily-snapshot",
ttl: 604800, // 7 days
});期限切れオブジェクトを自動削除するには、backups/ プレフィックスに R2 オブジェクトのライフサイクルルール を追加します。最長 TTL が 7 日なら、7 日より古いオブジェクトを期限切れにします。
アーカイブは backups/{backupId}/data.sqsh と backups/{backupId}/meta.json に置かれます。
if (previousBackup) {
await env.BACKUP_BUCKET.delete([
`backups/${previousBackup.id}/data.sqsh`,
`backups/${previousBackup.id}/meta.json`,
]);
}
const backup = await sandbox.createBackup({
dir: "/workspace",
name: "latest",
});
await env.KV.put("latest-backup", JSON.stringify(backup));if (previousBackup) {
await env.BACKUP_BUCKET.delete([
`backups/${previousBackup.id}/data.sqsh`,
`backups/${previousBackup.id}/meta.json`,
]);
}
const backup = await sandbox.createBackup({
dir: "/workspace",
name: "latest",
});
await env.KV.put("latest-backup", JSON.stringify(backup));await env.BACKUP_BUCKET.delete([
`backups/${backup.id}/data.sqsh`,
`backups/${backup.id}/meta.json`,
]);await env.BACKUP_BUCKET.delete([
`backups/${backup.id}/data.sqsh`,
`backups/${backup.id}/meta.json`,
]);backups/ 配下のオブジェクトを一覧し、アップロード時刻で削除します。
const listed = await env.BACKUP_BUCKET.list({ prefix: "backups/" });
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
for (const object of listed.objects) {
const ageMs = Date.now() - object.uploaded.getTime();
if (ageMs > sevenDaysMs) {
await env.BACKUP_BUCKET.delete(object.key);
}
}const listed = await env.BACKUP_BUCKET.list({ prefix: "backups/" });
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
for (const object of listed.objects) {
const ageMs = Date.now() - object.uploaded.getTime();
if (ageMs > sevenDaysMs) {
await env.BACKUP_BUCKET.delete(object.key);
}
}localBucket: true を渡すと、wrangler dev は BACKUP_BUCKET バインディングを使います。署名付き URL の認証情報は不要です。
const backup = await sandbox.createBackup({
dir: "/workspace",
localBucket: Boolean(env.LOCAL_DEV),
});
const result = await sandbox.restoreBackup(backup);const backup = await sandbox.createBackup({
dir: "/workspace",
localBucket: Boolean(env.LOCAL_DEV),
});
const result = await sandbox.restoreBackup(backup);ローカルの復元は unsquashfs でアーカイブを展開し、ディレクトリを置き換えます。保存したハンドルの localBucket フィールドが復元経路を選びます。
createBackup() は対象ディレクトリ配下のすべてのファイルを読めなければなりません。モード 0600 のファイルや、別ユーザー所有のディレクトリは BackupCreateError の原因になります。
可能なときはイメージ側で権限を設定します。a+rX はファイルに読み取り権限、ディレクトリに実行権限を追加します。
RUN mkdir -p /home/sandbox && chmod -R a+rX /home/sandbox実行時にプロセスが制限の厳しいファイルを作る場合は、バックアップ前に直します。
await sandbox.exec("chmod -R a+rX /home/sandbox/.claude");
const backup = await sandbox.createBackup({ dir: "/home/sandbox" });import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
try {
const backup = await sandbox.createBackup({ dir: "/workspace" });
} catch (error) {
if (error.code === "INVALID_BACKUP_CONFIG") {
console.error("Configuration error:", error.message);
} else if (error.code === "BACKUP_CREATE_FAILED") {
console.error("Backup failed:", error.message);
}
}
try {
await sandbox.restoreBackup(backup);
} catch (error) {
if (error.code === "BACKUP_NOT_FOUND") {
console.error("Backup not found in R2:", error.message);
} else if (error.code === "BACKUP_EXPIRED") {
console.error("Backup TTL has elapsed:", error.message);
} else if (error.code === "BACKUP_RESTORE_FAILED") {
console.error("Restore failed:", error.message);
}
}import { getSandbox } from "@cloudflare/sandbox";
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
try {
const backup = await sandbox.createBackup({ dir: "/workspace" });
} catch (error) {
if (error.code === "INVALID_BACKUP_CONFIG") {
console.error("Configuration error:", error.message);
} else if (error.code === "BACKUP_CREATE_FAILED") {
console.error("Backup failed:", error.message);
}
}
try {
await sandbox.restoreBackup(backup);
} catch (error) {
if (error.code === "BACKUP_NOT_FOUND") {
console.error("Backup not found in R2:", error.message);
} else if (error.code === "BACKUP_EXPIRED") {
console.error("Backup TTL has elapsed:", error.message);
} else if (error.code === "BACKUP_RESTORE_FAILED") {
console.error("Restore failed:", error.message);
}
}- ディレクトリバックアップ - オーバーレイ復元、ローカル展開、
EXDEV - Backups API - メソッド、オプション、型
- Storage API - S3 互換バケットのマウント
- R2 ドキュメント - R2 バケットと認証情報
- R2 ライフサイクルルール - オブジェクトの自動削除