INTEGRITY Документация

Резервное копирование и восстановление

Создавайте моментальные снимки каталогов песочницы на определённый момент времени и восстанавливайте их с помощью оверлеев copy-on-write. Резервные копии хранятся в бакете R2 и используют сжатие squashfs.

Используйте резервное копирование и восстановление, если вы хотите сохранить состояние рабочего пространства, например /workspace, чтобы вернуться позже. Этот вариант часто лучше подходит для постоянных каталогов проекта, тогда как монтирование bucket лучше подходит для отдельных постоянных путей хранения. Если вы монтируете bucket поверх /workspace, учитывайте, что в продакшене оно может перекрывать файлы, изначально заданные вашим образом.

Предварительные требования

  1. Создайте bucket R2 для хранения резервных копий:

    npx wrangler r2 bucket create my-backup-bucket
  2. Добавьте 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).

  3. Задайте учётные данные 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}`);

Особенности локальной разработки

Очистка объектов резервных копий в 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".

Распространённые причины

Исправление прав доступа во время сборки

Рекомендуемый подход: задать права доступа в вашем 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(). Проверьте права доступа всех файлов и подкаталогов в этом пути.

Рекомендации