跳转到内容
搜索文档

备份与恢复

最后更新 查看 MarkdownAgent 设置

使用写时复制叠加层创建沙箱目录的时间点快照并恢复它们。备份存储在 R2 存储桶中,并使用 squashfs 压缩。

当你希望工作区状态(例如 /workspace)稍后恢复时,请使用备份与恢复。这通常更适合持久项目目录,而存储桶挂载更适合独立的持久存储路径。如果在 /workspace 上挂载存储桶,请注意它可能在生产中覆盖镜像中预置的文件。

前提条件

  1. 创建用于存储备份的 R2 存储桶:

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

    {
    	"name": "my-sandbox-worker",
    	"main": "src/index.ts",
    	// Set this to today's date
    	"compatibility_date": "2026-08-17",
    	"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-08-17"
    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"

    如果你的 R2 存储桶使用特定司法管辖区端点,也可以将 BACKUP_BUCKET_ENDPOINT 添加到 vars,以覆盖默认的预签名 URL 端点(例如,欧盟区域存储桶使用 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(管理 R2 API 令牌) 下创建 R2 API 令牌。该令牌需要对备份存储桶具有 Object Read & Write(对象读取和写入) 权限。

创建备份

使用 createBackup() 对目录创建快照并将其上传到 R2:

import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Create a backup of /workspace
const backup = await sandbox.createBackup({ dir: "/workspace" });
console.log(`Backup created: ${backup.id}`);
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Create a backup of /workspace
const backup = await sandbox.createBackup({ dir: "/workspace" });
console.log(`Backup created: ${backup.id}`);

SDK 会创建该目录的压缩 squashfs 归档,并使用预签名 URL 直接上传到你的 R2 存储桶。

恢复备份

使用 restoreBackup() 从备份恢复目录:

import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Create a backup
const backup = await sandbox.createBackup({ dir: "/workspace" });

// Restore the backup
const result = await sandbox.restoreBackup(backup);
console.log(`Restored: ${result.success}`);
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Create a backup
const backup = await sandbox.createBackup({ dir: "/workspace" });

// Restore the backup
const result = await sandbox.restoreBackup(backup);
console.log(`Restored: ${result.success}`);

排除 gitignored 文件

在 git 仓库内的目录备份时,设置 useGitignore: true 可排除匹配 .gitignore 规则的文件。这对于跳过可重新生成的大型生成目录(如 node_modules/dist/build/)很有用。

import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Back up only tracked and untracked non-ignored files
const backup = await sandbox.createBackup({
	dir: "/workspace",
	useGitignore: true,
});
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Back up only tracked and untracked non-ignored files
const backup = await sandbox.createBackup({
	dir: "/workspace",
	useGitignore: true,
});

SDK 使用 git ls-files 解析哪些文件被忽略。根级与嵌套的 .gitignore 文件都会被遵守。

默认情况下,useGitignorefalse,目录中的所有文件都会包含在备份中。

检查点与回滚

在高风险操作前保存状态,并在失败时恢复:

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Save checkpoint before risky operation
const checkpoint = await sandbox.createBackup({ dir: "/workspace" });

try {
	await sandbox.exec("npm install some-experimental-package");
	await sandbox.exec("npm run build");
} catch (error) {
	// Restore to checkpoint if something goes wrong
	await sandbox.restoreBackup(checkpoint);
	console.log("Rolled back to checkpoint");
}
const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Save checkpoint before risky operation
const checkpoint = await sandbox.createBackup({ dir: "/workspace" });

try {
	await sandbox.exec("npm install some-experimental-package");
	await sandbox.exec("npm run build");
} catch (error) {
	// Restore to checkpoint if something goes wrong
	await sandbox.restoreBackup(checkpoint);
	console.log("Rolled back to checkpoint");
}

存储备份句柄

DirectoryBackup 句柄可序列化。可将其持久化到 KV、D1 或 Durable Object 存储以供稍后使用:

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Create a backup and store the handle in KV
const backup = await sandbox.createBackup({
	dir: "/workspace",
	name: "deploy-v2",
	ttl: 604800, // 7 days
});

await env.KV.put(`backup:${userId}`, JSON.stringify(backup));

// Later, retrieve and restore
const stored = await env.KV.get(`backup:${userId}`);
if (stored) {
	const backupHandle = JSON.parse(stored);
	await sandbox.restoreBackup(backupHandle);
}
const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Create a backup and store the handle in KV
const backup = await sandbox.createBackup({
	dir: "/workspace",
	name: "deploy-v2",
	ttl: 604800, // 7 days
});

await env.KV.put(`backup:${userId}`, JSON.stringify(backup));

// Later, retrieve and restore
const stored = await env.KV.get(`backup:${userId}`);
if (stored) {
	const backupHandle = JSON.parse(stored);
	await sandbox.restoreBackup(backupHandle);
}

使用命名备份

添加 name 选项以标识备份。名称最长 256 个字符:

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

const backup = await sandbox.createBackup({
	dir: "/workspace",
	name: "before-migration",
});

console.log(`Backup ID: ${backup.id}`);
const sandbox = getSandbox(env.Sandbox, "my-sandbox");

const backup = await sandbox.createBackup({
	dir: "/workspace",
	name: "before-migration",
});

console.log(`Backup ID: ${backup.id}`);

配置 TTL

为备份设置自定义生存时间。默认 TTL 为 3 天(259200 秒)。ttl 值必须为正秒数:

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Short-lived backup for a quick operation
const shortBackup = await sandbox.createBackup({
	dir: "/workspace",
	ttl: 600, // 10 minutes
});

// Long-lived backup for extended workflows
const longBackup = await sandbox.createBackup({
	dir: "/workspace",
	name: "daily-snapshot",
	ttl: 604800, // 7 days
});
const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Short-lived backup for a quick operation
const shortBackup = await sandbox.createBackup({
	dir: "/workspace",
	ttl: 600, // 10 minutes
});

// Long-lived backup for extended workflows
const longBackup = await sandbox.createBackup({
	dir: "/workspace",
	name: "daily-snapshot",
	ttl: 604800, // 7 days
});

TTL 的强制执行方式

TTL 在恢复时强制执行,而不是在创建时。当你调用 restoreBackup() 时,SDK 会从 R2 读取备份元数据,并将创建时间戳加上 TTL 与当前时间比较(带有 60 秒缓冲以防止竞态条件)。如果 TTL 已过期,恢复会以 BACKUP_EXPIRED 错误被拒绝。

TTL 不会自动从 R2 删除备份对象。过期备份仍保留在存储桶中,并继续占用存储,直到你显式删除它们或配置自动清理规则。

配置 R2 生命周期规则以自动清理

要自动从 R2 移除过期的备份对象,请在备份存储桶上设置 R2 对象生命周期规则。这是防止过期备份无限累积的推荐方式。

例如,如果你的最长 TTL 为 7 天,请配置生命周期规则,删除 backups/ 前缀下超过 7 天的对象。这可确保 R2 存储不会无限增长,同时为你恢复任何未过期备份留出缓冲。

本地开发

你可以在使用 wrangler dev 进行本地开发时,通过向 createBackup() 传递 localBucket: true 选项来使用备份与恢复。这会直接使用 Worker 环境中的 BACKUP_BUCKET R2 绑定,因此不需要预签名 URL 凭据。

配置 R2 绑定

BACKUP_BUCKET R2 绑定添加到 Wrangler 配置:

{
	"r2_buckets": [
		{
			"binding": "BACKUP_BUCKET",
			"bucket_name": "my-backup-bucket"
		}
	]
}
[[r2_buckets]]
binding = "BACKUP_BUCKET"
bucket_name = "my-backup-bucket"

使用 localBucket 备份与恢复

createBackup() 传递 localBucket: true,以直接使用 R2 绑定进行备份与恢复:

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Create a local backup
const backup = await sandbox.createBackup({
	dir: "/workspace",
	localBucket: true,
});

// Restore the backup
const result = await sandbox.restoreBackup(backup);
console.log(`Restored: ${result.success}`);
const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Create a local backup
const backup = await sandbox.createBackup({
	dir: "/workspace",
	localBucket: true,
});

// Restore the backup
const result = await sandbox.restoreBackup(backup);
console.log(`Restored: ${result.success}`);

本地开发注意事项

  • 无需预签名 URL — SDK 通过 R2 绑定直接读写备份归档,因此不需要兼容 S3 的凭据。
  • 无需 FUSE — 本地恢复使用 unsquashfs 提取归档,而不是用 FUSE overlayfs 挂载。备份不会作为写时复制叠加层应用;恢复时会替换目录。
  • 相同的归档格式 — 本地备份使用与生产备份相同的 squashfs 归档格式。

清理 R2 中的备份对象

备份归档存储在 R2 存储桶的 backups/ 前缀下,结构为 backups/{backupId}/data.sqshbackups/{backupId}/meta.json。你可以使用 BACKUP_BUCKET R2 绑定直接管理这些对象。

替换最新备份(先删后写)

如果你只需要最近的备份,请在创建新备份之前删除上一个:

import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Delete the previous backup's R2 objects before creating a new one
if (previousBackup) {
	await env.BACKUP_BUCKET.delete(`backups/${previousBackup.id}/data.sqsh`);
	await env.BACKUP_BUCKET.delete(`backups/${previousBackup.id}/meta.json`);
}

// Create a fresh backup
const backup = await sandbox.createBackup({
	dir: "/workspace",
	name: "latest",
});

// Store the handle so you can delete it next time
await env.KV.put("latest-backup", JSON.stringify(backup));
import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Delete the previous backup's R2 objects before creating a new one
if (previousBackup) {
	await env.BACKUP_BUCKET.delete(`backups/${previousBackup.id}/data.sqsh`);
	await env.BACKUP_BUCKET.delete(`backups/${previousBackup.id}/meta.json`);
}

// Create a fresh backup
const backup = await sandbox.createBackup({
	dir: "/workspace",
	name: "latest",
});

// Store the handle so you can delete it next time
await env.KV.put("latest-backup", JSON.stringify(backup));

按前缀列出并删除旧备份

要清理多个旧备份,请列出 backups/ 前缀下的对象并按键删除:

// List all backup objects in the bucket
const listed = await env.BACKUP_BUCKET.list({ prefix: "backups/" });

for (const object of listed.objects) {
	// Parse the backup ID from the key (backups/{id}/data.sqsh or backups/{id}/meta.json)
	const parts = object.key.split("/");
	const backupId = parts[1];

	// Delete objects older than 7 days
	const ageMs = Date.now() - object.uploaded.getTime();
	const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
	if (ageMs > sevenDaysMs) {
		await env.BACKUP_BUCKET.delete(object.key);
		console.log(`Deleted expired object: ${object.key}`);
	}
}
// List all backup objects in the bucket
const listed = await env.BACKUP_BUCKET.list({ prefix: "backups/" });

for (const object of listed.objects) {
	// Parse the backup ID from the key (backups/{id}/data.sqsh or backups/{id}/meta.json)
	const parts = object.key.split("/");
	const backupId = parts[1];

	// Delete objects older than 7 days
	const ageMs = Date.now() - object.uploaded.getTime();
	const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
	if (ageMs > sevenDaysMs) {
		await env.BACKUP_BUCKET.delete(object.key);
		console.log(`Deleted expired object: ${object.key}`);
	}
}

按 ID 删除特定备份

如果你有备份 ID,可直接删除其归档与元数据:

const backupId = backup.id;

await env.BACKUP_BUCKET.delete(`backups/${backupId}/data.sqsh`);
await env.BACKUP_BUCKET.delete(`backups/${backupId}/meta.json`);
const backupId = backup.id;

await env.BACKUP_BUCKET.delete(`backups/${backupId}/data.sqsh`);
await env.BACKUP_BUCKET.delete(`backups/${backupId}/meta.json`);

写时复制行为

在生产环境中,恢复使用 FUSE overlayfs 将备份挂载为只读下层。新写入进入可写上层,不会影响原始备份:

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Create a backup
const backup = await sandbox.createBackup({ dir: "/workspace" });

// Restore the backup
await sandbox.restoreBackup(backup);

// New writes go to the upper layer — the backup is unchanged
await sandbox.writeFile(
	"/workspace/new-file.txt",
	"This does not modify the backup",
);

// Restore the same backup again to discard changes
await sandbox.restoreBackup(backup);
const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Create a backup
const backup = await sandbox.createBackup({ dir: "/workspace" });

// Restore the backup
await sandbox.restoreBackup(backup);

// New writes go to the upper layer — the backup is unchanged
await sandbox.writeFile(
	"/workspace/new-file.txt",
	"This does not modify the backup",
);

// Restore the same backup again to discard changes
await sandbox.restoreBackup(backup);

处理错误

备份与恢复操作可能抛出特定错误。请在 try...catch 块中包装调用:

import { getSandbox } from "@cloudflare/sandbox";

const sandbox = getSandbox(env.Sandbox, "my-sandbox");

// Handle backup errors
try {
	const backup = await sandbox.createBackup({ dir: "/workspace" });
} catch (error) {
	if (error.code === "INVALID_BACKUP_CONFIG") {
		// Missing BACKUP_BUCKET binding or invalid directory path
		console.error("Configuration error:", error.message);
	} else if (error.code === "BACKUP_CREATE_FAILED") {
		// Archive creation or upload to R2 failed
		console.error("Backup failed:", error.message);
	}
}

// Handle restore errors
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");

// Handle backup errors
try {
	const backup = await sandbox.createBackup({ dir: "/workspace" });
} catch (error) {
	if (error.code === "INVALID_BACKUP_CONFIG") {
		// Missing BACKUP_BUCKET binding or invalid directory path
		console.error("Configuration error:", error.message);
	} else if (error.code === "BACKUP_CREATE_FAILED") {
		// Archive creation or upload to R2 failed
		console.error("Backup failed:", error.message);
	}
}

// Handle restore errors
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);
	}
}

路径权限

createBackup() 方法使用 mksquashfs 创建目标目录的压缩归档。此过程必须能够读取你要备份的路径中的每个文件与子目录。如果任何文件或目录具有阻止归档器读取的限制性权限,备份会失败并出现 BackupCreateError 与 “Permission denied” 消息。

常见原因

  • 由其他用户拥有的目录 — 如果目标目录包含由不同用户或进程创建的子目录(例如 /home/sandbox/.claude),归档器可能没有读取权限。
  • 限制性文件模式 — 模式为 0600 的文件或 0700 的目录,且属于与运行备份进程不同的用户。
  • 运行时生成的配置目录 — 工具与应用通常会创建配置目录(如 .cache.config 或工具特定的点文件)并带有限制性权限。

在构建时修复权限

推荐方法是在 Dockerfile 中设置权限,使每个容器都以正确的访问权限启动。这可避免在每次备份前于运行时执行 chmod

# Ensure the backup target directory is readable
RUN mkdir -p /home/sandbox && chmod -R a+rX /home/sandbox

a+rX 标志为所有文件授予读取权限,并为所有目录授予执行(遍历)权限,且不更改写权限。

在运行时修复权限

如果限制性权限来自运行时创建的文件(例如以 0600 模式生成配置文件的工具),请在调用 createBackup() 之前修复它们:

await sandbox.exec("chmod -R a+rX /home/sandbox/.claude");
const backup = await sandbox.createBackup({ dir: "/home/sandbox" });

示例错误

如果备份遇到权限问题,你会看到类似如下的错误:

BackupCreateError: mksquashfs failed: Could not create destination file: Permission denied

这意味着 mksquashfs 无法读取你传给 createBackup() 的目录中的一个或多个文件。请检查该路径内所有文件与子目录的权限。

最佳实践

  • 恢复前停止写入 — 在调用 restoreBackup() 之前,停止向目标目录写入的进程
  • 使用检查点 — 在包安装或迁移等高风险操作前创建备份
  • 排除 gitignored 文件 — 备份 git 仓库时设置 useGitignore: true,以跳过 node_modules/ 等生成文件并减小备份大小
  • 设置合适的 TTL — 临时检查点使用较短 TTL,持久快照使用较长 TTL
  • 在外部存储句柄 — 将 DirectoryBackup 句柄持久化到 KV、D1 或 Durable Object 存储,以便跨请求访问
  • 配置 R2 生命周期规则 — 设置 对象生命周期规则,自动从 R2 删除过期备份,因为 TTL 仅在恢复时强制执行
  • 清理旧备份 — 当不再需要时,从 R2 删除先前的备份对象,或对滚动备份使用先删后写模式
  • 处理错误 — 在 try...catch 块中包装备份与恢复调用
  • 重启后重新恢复 — 在生产环境中,FUSE 挂载是临时的,因此在容器重启后请从备份句柄重新恢复

相关资源

这篇文档对您有帮助吗?