Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

バックアップと復元

最終更新 Markdown で表示Agent セットアップ

このガイドでは、サンドボックスのディレクトリを R2 にスナップショットし、後から復元する方法を説明します。

サンドボックスがスリープしたあとも /workspace のようなプロジェクトディレクトリを戻したいときに、バックアップと復元を使います。永続ストレージを別パスにする場合は、代わりにバケットをマウントします。本番で /workspace にバケットをマウントすると、イメージがあらかじめ置いたファイルの上にマウントが重なります。

本番の復元がオーバーレイを使う理由は、ディレクトリバックアップ を参照してください。

前提条件

  1. R2 バケットを作成します。

    npx wrangler r2 bucket create my-backup-bucket
  2. Wrangler 設定に BACKUP_BUCKET R2 バインディングと署名付き 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)専用エンドポイントを使う場合は、varsBACKUP_BUCKET_ENDPOINT を追加します。EU バケットでは https://<ACCOUNT_ID>.eu.r2.cloudflarestorage.com を使います。

  3. 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 です。このフィールドは、当初バックアップしたディレクトリとは別の、許可されたディレクトリを指せます。

生成キャッシュを除外する

本番復元のあと、復元したツリー内でディレクトリ名を変更すると、EXDEVcross-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 では起きません。オーバーレイ復元は ディレクトリバックアップ を参照してください。

gitignore 対象のファイルを除外する

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));
}

名前と TTL を設定する

名前は最大 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.sqshbackups/{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));

ID でバックアップを削除する

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 devBACKUP_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);
	}
}

関連リソース

役に立ちましたか?