← Sandbox SDK / sandbox / guides
Резервное копирование и восстановление
Создавайте моментальные снимки каталогов песочницы на определённый момент времени и восстанавливайте их с помощью оверлеев copy-on-write. Резервные копии хранятся в бакете R2 и используют сжатие squashfs.
Используйте резервное копирование и восстановление, если вы хотите сохранить состояние рабочего пространства, например /workspace, чтобы вернуться позже. Этот вариант часто лучше подходит для постоянных каталогов проекта, тогда как монтирование bucket лучше подходит для отдельных постоянных путей хранения. Если вы монтируете bucket поверх /workspace, учитывайте, что в продакшене оно может перекрывать файлы, изначально заданные вашим образом.
Предварительные требования
-
Создайте bucket R2 для хранения резервных копий:
npx wrangler r2 bucket create my-backup-bucket -
Добавьте
BACKUP_BUCKETпривязку R2 и учетные данные presigned URL в конфигурацию Wrangler:{ "name": "my-sandbox-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-28", "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-28" 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 bucket использует endpoint, привязанный к юрисдикции, вы также можете добавить
BACKUP_BUCKET_ENDPOINTкvarsчтобы переопределить конечную точку presigned URL по умолчанию (например,https://<ACCOUNT_ID>.eu.r2.cloudflarestorage.comдля бакета в регионе EU). -
Задайте учётные данные R2 API как секреты:
npx wrangler secret put R2_ACCESS_KEY_ID npx wrangler secret put R2_SECRET_ACCESS_KEYТокены R2 API можно создать в Панель управления Cloudflare ↗ в разделе R2 > Обзор > Управление API-токенами R2. Токену требуется Object Read & Write разрешения для вашего bucket с резервными копиями.
Создайте резервную копию
Используйте 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 для каталога и загружает его напрямую в ваш бакет R2 с помощью предварительно подписанного URL.
Восстановление резервной копии
Используйте 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}`);Исключение файлов из .gitignore
При резервном копировании каталога внутри репозитория 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
Задайте собственное время жизни (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. Резервные копии с истёкшим сроком остаются в вашем bucket и продолжают занимать место в хранилище, пока вы не удалите их вручную или не настроите правило автоматической очистки.
Настройте правила жизненного цикла R2 для автоматической очистки
Чтобы автоматически удалять просроченные объекты резервных копий из R2, настройте Правило жизненного цикла объекта R2 для вашего бакета резервных копий. Это рекомендуемый способ не допустить бесконечного накопления истёкших резервных копий.
Например, если наибольший TTL составляет 7 дней, настройте правило жизненного цикла для удаления объектов старше 7 дней из backups/ префикс. Это гарантирует, что хранилище R2 не будет расти бесконечно, и при этом оставляет запас времени для восстановления любой ещё не истёкшей резервной копии.
Локальная разработка
При локальной разработке резервное копирование и восстановление можно выполнять с помощью wrangler dev передав localBucket: true опцию в createBackup(). Здесь используется BACKUP_BUCKET привязку R2 непосредственно из окружения вашего Worker, поэтому учетные данные presigned 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
Передайте localBucket: true к createBackup() для резервного копирования и восстановления напрямую через привязку 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.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
Если у вас есть 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, либо dotfiles конкретных инструментов) с ограничивающими правами доступа.
Исправление прав доступа во время сборки
Рекомендуемый подход: задать права доступа в вашем 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() - Используйте контрольные точки - Создавайте резервные копии перед рискованными операциями, такими как установка пакетов или миграции
- Исключение файлов из .gitignore - Задайте
useGitignore: trueпри резервном копировании git-репозиториев, чтобы пропускать сгенерированные файлы, такие какnode_modules/и уменьшить размер резервной копии - Задайте подходящие значения TTL - Используйте короткие TTL для временных контрольных точек и более длительные TTL для постоянных снимков
- Внешнее хранение дескрипторов - Сохраните
DirectoryBackupхендлы в KV, D1 или хранилище Durable Object для доступа между запросами - Настройте правила жизненного цикла R2 - Настройте правила жизненного цикла объекта чтобы автоматически удалять просроченные резервные копии из R2, поскольку TTL применяется только при восстановлении
- Очистка старых резервных копий - Удаляйте предыдущие объекты резервных копий из R2, когда они больше не нужны, либо используйте схему «удалить и затем записать» для скользящих резервных копий
- Обработка ошибок - Оборачивайте вызовы резервного копирования и восстановления в
try...catchблокирует - Повторное восстановление после перезапуска - В продакшене монтирование FUSE является временным, поэтому после перезапуска контейнера восстанавливайте данные заново из дескриптора резервной копии
Дополнительные материалы
- Справочник API резервных копий - Полная документация по методам
- Справочник Storage API - Монтируйте бакеты, совместимые с S3
- Документация R2 - Узнайте больше о Cloudflare R2
- Правила жизненного цикла R2 - Настройте автоматическую очистку объектов