使用写时复制叠加层创建沙箱目录的时间点快照并恢复它们。备份存储在 R2 存储桶中,并使用 squashfs 压缩。
当你希望工作区状态(例如 /workspace)稍后恢复时,请使用备份与恢复。这通常更适合持久项目目录,而存储桶挂载更适合独立的持久存储路径。如果在 /workspace 上挂载存储桶,请注意它可能在生产中覆盖镜像中预置的文件。
-
创建用于存储备份的 R2 存储桶:
npx wrangler r2 bucket create my-backup-bucket -
将
BACKUP_BUCKETR2 绑定与预签名 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)。 -
将 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}`);在 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 文件都会被遵守。
默认情况下,useGitignore 为 false,目录中的所有文件都会包含在备份中。
在高风险操作前保存状态,并在失败时恢复:
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 为 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 在恢复时强制执行,而不是在创建时。当你调用 restoreBackup() 时,SDK 会从 R2 读取备份元数据,并将创建时间戳加上 TTL 与当前时间比较(带有 60 秒缓冲以防止竞态条件)。如果 TTL 已过期,恢复会以 BACKUP_EXPIRED 错误被拒绝。
TTL 不会自动从 R2 删除备份对象。过期备份仍保留在存储桶中,并继续占用存储,直到你显式删除它们或配置自动清理规则。
要自动从 R2 移除过期的备份对象,请在备份存储桶上设置 R2 对象生命周期规则。这是防止过期备份无限累积的推荐方式。
例如,如果你的最长 TTL 为 7 天,请配置生命周期规则,删除 backups/ 前缀下超过 7 天的对象。这可确保 R2 存储不会无限增长,同时为你恢复任何未过期备份留出缓冲。
你可以在使用 wrangler dev 进行本地开发时,通过向 createBackup() 传递 localBucket: true 选项来使用备份与恢复。这会直接使用 Worker 环境中的 BACKUP_BUCKET R2 绑定,因此不需要预签名 URL 凭据。
将 BACKUP_BUCKET R2 绑定添加到 Wrangler 配置:
{
"r2_buckets": [
{
"binding": "BACKUP_BUCKET",
"bucket_name": "my-backup-bucket"
}
]
}[[r2_buckets]]
binding = "BACKUP_BUCKET"
bucket_name = "my-backup-bucket"向 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 存储桶的 backups/ 前缀下,结构为 backups/{backupId}/data.sqsh 与 backups/{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,可直接删除其归档与元数据:
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/sandboxa+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 挂载是临时的,因此在容器重启后请从备份句柄重新恢复