本指南说明如何将沙箱目录快照到 R2,并在稍后恢复。
当你希望 /workspace 这类项目目录在 sandbox 休眠后还能回来时,请使用备份与恢复。对于独立的持久存储路径,请改为挂载存储桶。如果在 /workspace 上挂载存储桶,该挂载会在生产环境中覆盖镜像中预置的文件。
有关生产环境恢复为何使用 overlay,请参阅 目录备份。
-
创建 R2 存储桶:
npx wrangler r2 bucket create my-backup-bucket -
将
BACKUP_BUCKETR2 绑定与预签名 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。 -
将 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 恢复,请参阅 目录备份。
要在 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 解压归档并替换目录。已存储 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);
}
}- 目录备份 - overlay 恢复、本地解压与
EXDEV - Backups API - 方法、选项与类型
- 存储 API - 挂载兼容 S3 的存储桶
- R2 文档 - R2 存储桶与凭据
- R2 生命周期规则 - 自动对象清理