跳转到内容
本站为社区维护的非官方中文翻译,与 Cloudflare, Inc. 无关联。本站不提供账户登录,请勿在任何非官方站点输入您的 Cloudflare 账户信息。官方文档:developers.cloudflare.com

备份与恢复

最后更新 查看 MarkdownAgent 设置

本指南说明如何将沙箱目录快照到 R2,并在稍后恢复。

当你希望 /workspace 这类项目目录在 sandbox 休眠后还能回来时,请使用备份与恢复。对于独立的持久存储路径,请改为挂载存储桶。如果在 /workspace 上挂载存储桶,该挂载会在生产环境中覆盖镜像中预置的文件。

有关生产环境恢复为何使用 overlay,请参阅 目录备份。

前提条件

  1. 创建 R2 存储桶:

    npx wrangler r2 bucket create my-backup-bucket
  2. 将 BACKUP_BUCKET R2 绑定与预签名 URL 设置添加到 Wrangler 配置:

    {
    	"name": "my-sandbox-worker",
    	"main": "src/index.ts",
        "upload_source_maps": true,
    	// Set this to today's date
    	"compatibility_date": "2026-11-14",
    	"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"
    upload_source_maps = true
    # Set this to today's date
    compatibility_date = "2026-11-14"
    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"

    如果存储桶使用特定管辖区端点,请将 BACKUP_BUCKET_ENDPOINT 添加到 vars。对于欧盟存储桶,使用 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);

在生产环境中,恢复会挂载写时复制 overlay。当 sandbox 休眠或容器重启时,该挂载会丢失。请从已存储的 handle 再次恢复。

恢复目标是 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 会解压归档,因此不会出现此失败。有关 overlay 恢复,请参阅 目录备份。

排除 gitignored 文件

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

存储备份 handle

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

按 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 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 解压归档并替换目录。已存储 handle 的 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);
	}
}

相关资源

这篇文档对您有帮助吗?