From 3faeb7a72b9604c8691a0beb175cddbe951bfa23 Mon Sep 17 00:00:00 2001 From: "ask-bonk[bot]" Date: Thu, 19 Feb 2026 17:40:21 +0000 Subject: [PATCH 01/10] [Sandbox] Add docs for backup and restore API Add documentation for the new createBackup() and restoreBackup() methods introduced in cloudflare/sandbox-sdk#396. - New API reference page: /sandbox/api/backups/ - New how-to guide: /sandbox/guides/backup-restore/ - Updated API index with Backups card - Updated Wrangler config page with BACKUP_BUCKET R2 binding docs Closes #28448 --- src/content/docs/sandbox/api/backups.mdx | 223 +++++++++++++ src/content/docs/sandbox/api/index.mdx | 5 + .../docs/sandbox/configuration/wrangler.mdx | 23 ++ .../docs/sandbox/guides/backup-restore.mdx | 294 ++++++++++++++++++ 4 files changed, 545 insertions(+) create mode 100644 src/content/docs/sandbox/api/backups.mdx create mode 100644 src/content/docs/sandbox/guides/backup-restore.mdx diff --git a/src/content/docs/sandbox/api/backups.mdx b/src/content/docs/sandbox/api/backups.mdx new file mode 100644 index 000000000000000..1632351d7deb116 --- /dev/null +++ b/src/content/docs/sandbox/api/backups.mdx @@ -0,0 +1,223 @@ +--- +title: Backups +pcx_content_type: concept +sidebar: + order: 5 +--- + +import { TypeScriptExample } from "~/components"; + +Create point-in-time snapshots of sandbox directories and restore them with copy-on-write overlays. + +## Methods + +### `createBackup()` + +Create a point-in-time snapshot of a directory and upload it to R2 storage. + +```ts +await sandbox.createBackup(options: BackupOptions): Promise +``` + +**Parameters**: + +- `options` - Backup configuration (see [`BackupOptions`](#backupoptions)): + - `dir` (required) - Absolute path to the directory to back up (for example, `"/workspace"`) + - `name` (optional) - Human-readable name for the backup. Maximum 256 characters, no control characters. + - `ttl` (optional) - Time-to-live in seconds until the backup expires. Default: `3600` (1 hour). Maximum: `86400` (24 hours). + +**Returns**: `Promise` containing: + +- `id` - Unique backup identifier (UUID) +- `dir` - Directory that was backed up + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// Create a backup of /workspace +const backup = await sandbox.createBackup({ dir: '/workspace' }); + +// Later, restore the backup +await sandbox.restoreBackup(backup); +``` + + +**How it works**: + +1. The container creates a compressed squashfs archive from the directory. +2. The archive is uploaded to R2, chunked for files over 50 MB. +3. Metadata is stored alongside the archive. +4. The local archive is cleaned up. + +**Throws**: +- `InvalidBackupConfigError` - If `dir` is not absolute, contains `..`, or the `BACKUP_BUCKET` binding is missing +- `BackupCreateError` - If the container fails to create the archive or it exceeds 500 MB + +:::note[R2 binding required] +You must configure a `BACKUP_BUCKET` R2 binding in your `wrangler.jsonc` before using backup methods. Refer to the [Wrangler configuration](/sandbox/configuration/wrangler/) for binding setup. +::: + +:::caution[Partial writes] +Partially-written files may not be captured consistently. Only completed writes are guaranteed to be included in the backup. +::: + +--- + +### `restoreBackup()` + +Restore a previously created backup into a directory using FUSE overlayfs (copy-on-write). + +```ts +await sandbox.restoreBackup(backup: DirectoryBackup): Promise +``` + +**Parameters**: + +- `backup` - The backup handle returned by `createBackup()`. Contains `id` and `dir`. (see [`DirectoryBackup`](#directorybackup)) + +**Returns**: `Promise` containing: + +- `success` - Whether the restore succeeded +- `dir` - Directory that was restored +- `backupId` - Backup ID that was restored + + +``` +// Create a named backup with 24-hour TTL +const backup = await sandbox.createBackup({ + dir: '/workspace', + name: 'before-refactor', + ttl: 86400 +}); + +// Store the handle for later use +await env.KV.put(`backup:${userId}`, JSON.stringify(backup)); +``` + + +**How it works**: + +1. The archive and metadata are downloaded from R2. +2. The TTL is checked. If expired, an error is thrown (with a 60-second buffer). +3. The archive is written to the container. +4. The container mounts the squashfs archive with FUSE overlayfs. + +**Throws**: +- `InvalidBackupConfigError` - If `backup.id` is missing or not a valid UUID, or `backup.dir` is invalid +- `BackupNotFoundError` - If the backup metadata or archive is not found in R2 +- `BackupExpiredError` - If the backup TTL has elapsed +- `BackupRestoreError` - If the container fails to restore + +:::note[Copy-on-write] +Restore uses copy-on-write semantics. The backup is mounted as a read-only lower layer, and new writes go to a writable upper layer. The backup can be restored into a different directory than the original. +::: + +:::caution[Ephemeral mount] +The FUSE mount is lost when the sandbox sleeps or restarts. Re-restore from the backup handle to recover. Stop processes writing to the target directory before restoring. +::: + +## Usage patterns + +### Checkpoint and restore + +Use backups as checkpoints before risky operations. + + +``` +// 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 the checkpoint if something goes wrong + await sandbox.restoreBackup(checkpoint); +} +``` + + +### Error handling + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +try { + const backup = await sandbox.createBackup({ dir: '/workspace' }); + console.log(`Backup created: ${backup.id}`); +} 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); + } +} +``` + + +## Behavior + +- The maximum archive size is 500 MB. +- Concurrent backup and restore operations on the same sandbox are automatically serialized. +- The returned `DirectoryBackup` handle is serializable — store it in KV, D1, or Durable Object storage. +- Overlapping backups are independent. Restoring a parent directory overwrites subdirectory mounts. + +## Types + +### `BackupOptions` + +```ts +interface BackupOptions { + dir: string; + name?: string; + ttl?: number; +} +``` + +**Fields**: + +- `dir` (required) - Absolute path to the directory to back up +- `name` (optional) - Human-readable backup name. Maximum 256 characters, no control characters. +- `ttl` (optional) - Time-to-live in seconds. Default: `3600`. Maximum: `86400`. + +### `DirectoryBackup` + +```ts +interface DirectoryBackup { + readonly id: string; + readonly dir: string; +} +``` + +**Fields**: + +- `id` - Unique backup identifier (UUID) +- `dir` - Directory that was backed up + +### `RestoreBackupResult` + +```ts +interface RestoreBackupResult { + success: boolean; + dir: string; + backupId: string; +} +``` + +**Fields**: + +- `success` - Whether the restore succeeded +- `dir` - Directory that was restored +- `backupId` - Backup ID that was restored + +## Related resources + +- [Storage API](/sandbox/api/storage/) - Mount S3-compatible buckets +- [Files API](/sandbox/api/files/) - Read and write files +- [Wrangler configuration](/sandbox/configuration/wrangler/) - Configure bindings diff --git a/src/content/docs/sandbox/api/index.mdx b/src/content/docs/sandbox/api/index.mdx index e0473f8f5739da6..66c9e1ec8ad7bd8 100644 --- a/src/content/docs/sandbox/api/index.mdx +++ b/src/content/docs/sandbox/api/index.mdx @@ -54,6 +54,11 @@ The Sandbox SDK provides a comprehensive API for executing code, managing files, data storage across sandbox lifecycles. + + Create point-in-time snapshots of directories and restore them with + copy-on-write overlays. Store backups in R2. + + Create isolated execution contexts within a sandbox. Each session maintains its own shell state, environment variables, and working directory. diff --git a/src/content/docs/sandbox/configuration/wrangler.mdx b/src/content/docs/sandbox/configuration/wrangler.mdx index 12baf25c8c207a4..a3a516763a05126 100644 --- a/src/content/docs/sandbox/configuration/wrangler.mdx +++ b/src/content/docs/sandbox/configuration/wrangler.mdx @@ -52,6 +52,29 @@ The Sandbox SDK is built on Cloudflare Containers. Your configuration requires t The minimal configuration shown above includes all required settings. For detailed configuration options, refer to the [Containers configuration documentation](/workers/wrangler/configuration/#containers). +## Backup storage + +To use the [backup and restore API](/sandbox/api/backups/), add an R2 bucket binding named `BACKUP_BUCKET`: + +```sh +npx wrangler r2 bucket create my-backup-bucket +``` + + +```jsonc +{ + "r2_buckets": [ + { + "binding": "BACKUP_BUCKET", + "bucket_name": "my-backup-bucket", + }, + ], +} +``` + + +The SDK automatically detects this binding and uses it for `createBackup()` and `restoreBackup()` operations. For a complete setup walkthrough, refer to the [backup and restore guide](/sandbox/guides/backup-restore/). + ## Troubleshooting ### Binding not found diff --git a/src/content/docs/sandbox/guides/backup-restore.mdx b/src/content/docs/sandbox/guides/backup-restore.mdx new file mode 100644 index 000000000000000..b4791bf55c2f223 --- /dev/null +++ b/src/content/docs/sandbox/guides/backup-restore.mdx @@ -0,0 +1,294 @@ +--- +title: Backup and restore +pcx_content_type: how-to +sidebar: + order: 11 +description: Create point-in-time backups and restore sandbox directories. +--- + +import { TypeScriptExample, WranglerConfig } from "~/components"; + +Create point-in-time snapshots of sandbox directories and restore them using copy-on-write overlays. Backups are stored in an R2 bucket and use squashfs compression. + +:::caution[Production only] +Backup and restore does not work with `wrangler dev` because it requires FUSE support that wrangler does not currently provide. Deploy your Worker with `wrangler deploy` to use this feature. All other Sandbox SDK features work in local development. +::: + +## Prerequisites + +Create an R2 bucket for storing backups: + +```sh +npx wrangler r2 bucket create my-backup-bucket +``` + +Add the `BACKUP_BUCKET` R2 binding to your Wrangler configuration: + + + +```jsonc +{ + "name": "my-sandbox-worker", + "main": "src/index.ts", + "compatibility_date": "$today", + "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", + }, + ], + "r2_buckets": [ + { + "binding": "BACKUP_BUCKET", + "bucket_name": "my-backup-bucket", + }, + ], +} +``` + + + +## Create a backup + +Use `createBackup()` to snapshot a directory and upload it to R2: + + + +```typescript +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}`); +``` + + + +The SDK creates a compressed squashfs archive of the directory and uploads it to your R2 bucket. The maximum archive size is 500 MB. + +## Restore a backup + +Use `restoreBackup()` to restore a directory from a backup: + + + +```typescript +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}`); +``` + + + +:::caution[Ephemeral mount] +The FUSE mount is lost when the sandbox sleeps or the container restarts. Re-restore from the backup handle to recover. +::: + +## Checkpoint and rollback + +Save state before risky operations and restore if something fails: + + + +```typescript +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"); +} +``` + + + +## Store backup handles + +The `DirectoryBackup` handle is serializable. Persist it to KV, D1, or Durable Object storage for later use: + + + +```typescript +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: 86400, +}); + +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); +} +``` + + + +## Use named backups + +Add a `name` option to identify backups. Names can be up to 256 characters: + + + +```typescript +const sandbox = getSandbox(env.Sandbox, "my-sandbox"); + +const backup = await sandbox.createBackup({ + dir: "/workspace", + name: "before-migration", +}); + +console.log(`Backup ID: ${backup.id}`); +``` + + + +## Configure TTL + +Set a custom time-to-live for backups. The default TTL is 1 hour (3600 seconds) and the maximum is 24 hours (86400 seconds): + + + +```typescript +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: 86400, // 24 hours +}); +``` + + + +:::note[Expired backups in R2] +Expired backups are not automatically deleted from R2. Configure [R2 lifecycle rules](/r2/buckets/object-lifecycles/) to clean up expired objects. +::: + +## Copy-on-write behavior + +Restore uses FUSE overlayfs to mount the backup as a read-only lower layer. New writes go to a writable upper layer and do not affect the original backup: + + + +```typescript +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); +``` + + + +## Handle errors + +Backup and restore operations can throw specific errors. Wrap calls in try/catch blocks: + + + +```typescript +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 failed or exceeds 500 MB limit + 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); + } +} +``` + + + +## Best practices + +- **Stop writes before restoring** - Stop processes writing to the target directory before calling `restoreBackup()` +- **Use checkpoints** - Create backups before risky operations like package installations or migrations +- **Set appropriate TTLs** - Use short TTLs for temporary checkpoints and longer TTLs for persistent snapshots +- **Store handles externally** - Persist `DirectoryBackup` handles to KV, D1, or Durable Object storage for cross-request access +- **Configure R2 lifecycle rules** - Set up [object lifecycle rules](/r2/buckets/object-lifecycles/) to clean up expired backups +- **Handle errors** - Wrap backup and restore calls in try/catch blocks +- **Keep archives small** - Back up only the directories you need to stay within the 500 MB limit +- **Re-restore after restart** - The FUSE mount is ephemeral, so re-restore from the backup handle after container restarts + +## Related resources + +- [Backups API reference](/sandbox/api/backups/) - Full method documentation +- [Storage API reference](/sandbox/api/storage/) - Mount S3-compatible buckets +- [R2 documentation](/r2/) - Learn about Cloudflare R2 +- [R2 lifecycle rules](/r2/buckets/object-lifecycles/) - Configure automatic object cleanup From b061e1566cb57ef77df5b953370b0507336259d5 Mon Sep 17 00:00:00 2001 From: "ask-bonk[bot]" Date: Fri, 20 Feb 2026 14:41:04 +0000 Subject: [PATCH 02/10] [Sandbox] Update backup docs for presigned URL flow, TTL enforcement, and R2 cleanup examples --- src/content/docs/sandbox/api/backups.mdx | 78 +++++++----- .../docs/sandbox/configuration/wrangler.mdx | 21 +++- .../docs/sandbox/guides/backup-restore.mdx | 118 ++++++++++++++++-- 3 files changed, 170 insertions(+), 47 deletions(-) diff --git a/src/content/docs/sandbox/api/backups.mdx b/src/content/docs/sandbox/api/backups.mdx index 1632351d7deb116..e95451f74fb5bea 100644 --- a/src/content/docs/sandbox/api/backups.mdx +++ b/src/content/docs/sandbox/api/backups.mdx @@ -24,7 +24,7 @@ await sandbox.createBackup(options: BackupOptions): Promise - `options` - Backup configuration (see [`BackupOptions`](#backupoptions)): - `dir` (required) - Absolute path to the directory to back up (for example, `"/workspace"`) - `name` (optional) - Human-readable name for the backup. Maximum 256 characters, no control characters. - - `ttl` (optional) - Time-to-live in seconds until the backup expires. Default: `3600` (1 hour). Maximum: `86400` (24 hours). + - `ttl` (optional) - Time-to-live in seconds until the backup expires. Default: `86400` (24 hours). Must be a positive number. **Returns**: `Promise` containing: @@ -42,22 +42,23 @@ const backup = await sandbox.createBackup({ dir: '/workspace' }); // Later, restore the backup await sandbox.restoreBackup(backup); -``` + +```` **How it works**: 1. The container creates a compressed squashfs archive from the directory. -2. The archive is uploaded to R2, chunked for files over 50 MB. -3. Metadata is stored alongside the archive. +2. The container uploads the archive directly to R2 using a presigned URL. +3. Metadata is stored alongside the archive in R2. 4. The local archive is cleaned up. **Throws**: -- `InvalidBackupConfigError` - If `dir` is not absolute, contains `..`, or the `BACKUP_BUCKET` binding is missing -- `BackupCreateError` - If the container fails to create the archive or it exceeds 500 MB +- `InvalidBackupConfigError` - If `dir` is not absolute, contains `..`, the `BACKUP_BUCKET` binding is missing, or the R2 presigned URL credentials are not configured +- `BackupCreateError` - If the container fails to create the archive or the upload to R2 fails -:::note[R2 binding required] -You must configure a `BACKUP_BUCKET` R2 binding in your `wrangler.jsonc` before using backup methods. Refer to the [Wrangler configuration](/sandbox/configuration/wrangler/) for binding setup. +:::note[R2 binding and credentials required] +You must configure a `BACKUP_BUCKET` R2 binding and R2 presigned URL credentials (`R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `CLOUDFLARE_ACCOUNT_ID`, `BACKUP_BUCKET_NAME`) in your `wrangler.jsonc` before using backup methods. Refer to the [Wrangler configuration](/sandbox/configuration/wrangler/) for binding setup. ::: :::caution[Partial writes] @@ -72,7 +73,7 @@ Restore a previously created backup into a directory using FUSE overlayfs (copy- ```ts await sandbox.restoreBackup(backup: DirectoryBackup): Promise -``` +```` **Parameters**: @@ -95,15 +96,15 @@ const backup = await sandbox.createBackup({ // Store the handle for later use await env.KV.put(`backup:${userId}`, JSON.stringify(backup)); + ``` **How it works**: -1. The archive and metadata are downloaded from R2. -2. The TTL is checked. If expired, an error is thrown (with a 60-second buffer). -3. The archive is written to the container. -4. The container mounts the squashfs archive with FUSE overlayfs. +1. Metadata is downloaded from R2 and the TTL is checked. If expired, an error is thrown (with a 60-second buffer). +2. The container downloads the archive directly from R2 using a presigned URL. +3. The container mounts the squashfs archive with FUSE overlayfs. **Throws**: - `InvalidBackupConfigError` - If `backup.id` is missing or not a valid UUID, or `backup.dir` is invalid @@ -127,16 +128,18 @@ Use backups as checkpoints before risky operations. ``` + // 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'); +await sandbox.exec('npm install some-experimental-package'); +await sandbox.exec('npm run build'); } catch (error) { - // Restore to the checkpoint if something goes wrong - await sandbox.restoreBackup(checkpoint); +// Restore to the checkpoint if something goes wrong +await sandbox.restoreBackup(checkpoint); } + ``` @@ -144,30 +147,37 @@ try { ``` -import { getSandbox } from '@cloudflare/sandbox'; + +import { getSandbox } from "@cloudflare/sandbox"; const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); try { - const backup = await sandbox.createBackup({ dir: '/workspace' }); - console.log(`Backup created: ${backup.id}`); +const backup = await sandbox.createBackup({ dir: '/workspace' }); +console.log(`Backup created: ${backup.id}`); } 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); - } +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); } -``` +} + +```` ## Behavior -- The maximum archive size is 500 MB. - Concurrent backup and restore operations on the same sandbox are automatically serialized. - The returned `DirectoryBackup` handle is serializable — store it in KV, D1, or Durable Object storage. - Overlapping backups are independent. Restoring a parent directory overwrites subdirectory mounts. +### TTL enforcement + +The `ttl` value controls when a backup is considered expired. The SDK enforces this at **restore time only** — when you call `restoreBackup()`, the SDK reads the backup metadata from R2 and checks whether the TTL has elapsed. If it has, the restore is rejected with a `BACKUP_EXPIRED` error. + +The TTL does **not** automatically delete objects from R2. Expired backup archives and metadata remain in your R2 bucket until you delete them. To automatically clean up expired objects, configure an [R2 object lifecycle rule](/r2/buckets/object-lifecycles/) on your backup bucket. Without a lifecycle rule, expired backups continue to consume R2 storage. + ## Types ### `BackupOptions` @@ -178,20 +188,20 @@ interface BackupOptions { name?: string; ttl?: number; } -``` +```` **Fields**: - `dir` (required) - Absolute path to the directory to back up - `name` (optional) - Human-readable backup name. Maximum 256 characters, no control characters. -- `ttl` (optional) - Time-to-live in seconds. Default: `3600`. Maximum: `86400`. +- `ttl` (optional) - Time-to-live in seconds. Default: `86400` (24 hours). Must be a positive number. ### `DirectoryBackup` ```ts interface DirectoryBackup { - readonly id: string; - readonly dir: string; + readonly id: string; + readonly dir: string; } ``` @@ -204,9 +214,9 @@ interface DirectoryBackup { ```ts interface RestoreBackupResult { - success: boolean; - dir: string; - backupId: string; + success: boolean; + dir: string; + backupId: string; } ``` diff --git a/src/content/docs/sandbox/configuration/wrangler.mdx b/src/content/docs/sandbox/configuration/wrangler.mdx index a3a516763a05126..32a4cf0ec333814 100644 --- a/src/content/docs/sandbox/configuration/wrangler.mdx +++ b/src/content/docs/sandbox/configuration/wrangler.mdx @@ -54,15 +54,23 @@ The minimal configuration shown above includes all required settings. For detail ## Backup storage -To use the [backup and restore API](/sandbox/api/backups/), add an R2 bucket binding named `BACKUP_BUCKET`: +To use the [backup and restore API](/sandbox/api/backups/), you need an R2 bucket binding and presigned URL credentials. The container uploads and downloads backup archives directly to/from R2 using presigned URLs, which requires R2 API token credentials. + +### 1. Create the R2 bucket ```sh npx wrangler r2 bucket create my-backup-bucket ``` +### 2. Add the binding and environment variables + ```jsonc { + "vars": { + "BACKUP_BUCKET_NAME": "my-backup-bucket", + "CLOUDFLARE_ACCOUNT_ID": "", + }, "r2_buckets": [ { "binding": "BACKUP_BUCKET", @@ -73,7 +81,16 @@ npx wrangler r2 bucket create my-backup-bucket ``` -The SDK automatically detects this binding and uses it for `createBackup()` and `restoreBackup()` operations. For a complete setup walkthrough, refer to the [backup and restore guide](/sandbox/guides/backup-restore/). +### 3. Set R2 API credentials as secrets + +```sh +npx wrangler secret put R2_ACCESS_KEY_ID +npx wrangler secret put R2_SECRET_ACCESS_KEY +``` + +Create an R2 API token in the [Cloudflare dashboard](https://dash.cloudflare.com/) under **R2** > **Overview** > **Manage R2 API Tokens**. The token needs **Object Read & Write** permissions for your backup bucket. + +The SDK uses these credentials to generate presigned URLs that allow the container to transfer backup archives directly to and from R2. For a complete setup walkthrough, refer to the [backup and restore guide](/sandbox/guides/backup-restore/). ## Troubleshooting diff --git a/src/content/docs/sandbox/guides/backup-restore.mdx b/src/content/docs/sandbox/guides/backup-restore.mdx index b4791bf55c2f223..ec0a8a7f0b613f5 100644 --- a/src/content/docs/sandbox/guides/backup-restore.mdx +++ b/src/content/docs/sandbox/guides/backup-restore.mdx @@ -22,7 +22,7 @@ Create an R2 bucket for storing backups: npx wrangler r2 bucket create my-backup-bucket ``` -Add the `BACKUP_BUCKET` R2 binding to your Wrangler configuration: +Add the `BACKUP_BUCKET` R2 binding and presigned URL credentials to your Wrangler configuration: @@ -52,6 +52,10 @@ Add the `BACKUP_BUCKET` R2 binding to your Wrangler configuration: "tag": "v1", }, ], + "vars": { + "BACKUP_BUCKET_NAME": "my-backup-bucket", + "CLOUDFLARE_ACCOUNT_ID": "", + }, "r2_buckets": [ { "binding": "BACKUP_BUCKET", @@ -63,6 +67,15 @@ Add the `BACKUP_BUCKET` R2 binding to your Wrangler configuration: +Then set your R2 API credentials as secrets: + +```sh +npx wrangler secret put R2_ACCESS_KEY_ID +npx wrangler secret put R2_SECRET_ACCESS_KEY +``` + +You can create R2 API tokens in the [Cloudflare dashboard](https://dash.cloudflare.com/) under **R2** > **Overview** > **Manage R2 API Tokens**. The token needs **Object Read & Write** permissions for your backup bucket. + ## Create a backup Use `createBackup()` to snapshot a directory and upload it to R2: @@ -81,7 +94,7 @@ console.log(`Backup created: ${backup.id}`); -The SDK creates a compressed squashfs archive of the directory and uploads it to your R2 bucket. The maximum archive size is 500 MB. +The SDK creates a compressed squashfs archive of the directory and uploads it directly to your R2 bucket using a presigned URL. ## Restore a backup @@ -145,7 +158,7 @@ const sandbox = getSandbox(env.Sandbox, "my-sandbox"); const backup = await sandbox.createBackup({ dir: "/workspace", name: "deploy-v2", - ttl: 86400, + ttl: 604800, // 7 days }); await env.KV.put(`backup:${userId}`, JSON.stringify(backup)); @@ -181,7 +194,7 @@ console.log(`Backup ID: ${backup.id}`); ## Configure TTL -Set a custom time-to-live for backups. The default TTL is 1 hour (3600 seconds) and the maximum is 24 hours (86400 seconds): +Set a custom time-to-live for backups. The default TTL is 24 hours (86400 seconds). The `ttl` value must be a positive number of seconds: @@ -198,15 +211,98 @@ const shortBackup = await sandbox.createBackup({ const longBackup = await sandbox.createBackup({ dir: "/workspace", name: "daily-snapshot", - ttl: 86400, // 24 hours + ttl: 604800, // 7 days }); ``` -:::note[Expired backups in R2] -Expired backups are not automatically deleted from R2. Configure [R2 lifecycle rules](/r2/buckets/object-lifecycles/) to clean up expired objects. -::: +### How TTL is enforced + +The TTL is enforced at **restore time**, not at creation time. When you call `restoreBackup()`, the SDK reads the backup metadata from R2 and compares the creation timestamp plus TTL against the current time. If the TTL has elapsed, the restore is rejected with a `BACKUP_EXPIRED` error. + +The TTL does **not** automatically delete backup objects from R2. Expired backups remain in your bucket and continue to consume storage until you explicitly delete them or configure an automatic cleanup rule. + +### Configure R2 lifecycle rules for automatic cleanup + +To automatically remove expired backup objects from R2, set up an [R2 object lifecycle rule](/r2/buckets/object-lifecycles/) on your backup bucket. This is the recommended way to prevent expired backups from accumulating indefinitely. + +For example, if your longest TTL is 7 days, configure a lifecycle rule to delete objects older than 7 days from the `backups/` prefix. This ensures R2 storage does not grow unbounded while giving you a buffer to restore any non-expired backup. + +## Clean up backup objects in R2 + +Backup archives are stored in your R2 bucket under the `backups/` prefix with the structure `backups/{backupId}/data.sqsh` and `backups/{backupId}/meta.json`. You can use the `BACKUP_BUCKET` R2 binding to manage these objects directly. + +### Replace the latest backup (delete-then-write) + +If you only need the most recent backup, delete the previous one before creating a new one: + + + +```typescript +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)); +``` + + + +### List and delete old backups by prefix + +To clean up multiple old backups, list objects under the `backups/` prefix and delete them by key: + + + +```typescript +// 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}`); + } +} +``` + + + +### Delete a specific backup by ID + +If you have the backup ID, delete both its archive and metadata directly: + + + +```typescript +const backupId = backup.id; + +await env.BACKUP_BUCKET.delete(`backups/${backupId}/data.sqsh`); +await env.BACKUP_BUCKET.delete(`backups/${backupId}/meta.json`); +``` + + ## Copy-on-write behavior @@ -254,7 +350,7 @@ try { // Missing BACKUP_BUCKET binding or invalid directory path console.error("Configuration error:", error.message); } else if (error.code === "BACKUP_CREATE_FAILED") { - // Archive creation failed or exceeds 500 MB limit + // Archive creation or upload to R2 failed console.error("Backup failed:", error.message); } } @@ -281,9 +377,9 @@ try { - **Use checkpoints** - Create backups before risky operations like package installations or migrations - **Set appropriate TTLs** - Use short TTLs for temporary checkpoints and longer TTLs for persistent snapshots - **Store handles externally** - Persist `DirectoryBackup` handles to KV, D1, or Durable Object storage for cross-request access -- **Configure R2 lifecycle rules** - Set up [object lifecycle rules](/r2/buckets/object-lifecycles/) to clean up expired backups +- **Configure R2 lifecycle rules** - Set up [object lifecycle rules](/r2/buckets/object-lifecycles/) to automatically delete expired backups from R2, since TTL is only enforced at restore time +- **Clean up old backups** - Delete previous backup objects from R2 when you no longer need them, or use the delete-then-write pattern for rolling backups - **Handle errors** - Wrap backup and restore calls in try/catch blocks -- **Keep archives small** - Back up only the directories you need to stay within the 500 MB limit - **Re-restore after restart** - The FUSE mount is ephemeral, so re-restore from the backup handle after container restarts ## Related resources From 6d22a732f8475f122e049ed7ce7ebd582a0b2c9f Mon Sep 17 00:00:00 2001 From: katereznykova Date: Fri, 20 Feb 2026 15:39:54 +0000 Subject: [PATCH 03/10] chore: trigger rebuild From 4fb63f3d4988e67eb5209b3f88b8e503082390de Mon Sep 17 00:00:00 2001 From: "ask-bonk[bot]" Date: Fri, 20 Feb 2026 18:12:48 +0000 Subject: [PATCH 04/10] [Sandbox] Fix backup docs to match SDK PR #396: correct default TTL to 3 days and RestoreBackupResult.id field name --- src/content/docs/sandbox/api/backups.mdx | 10 +++++----- src/content/docs/sandbox/guides/backup-restore.mdx | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/content/docs/sandbox/api/backups.mdx b/src/content/docs/sandbox/api/backups.mdx index e95451f74fb5bea..066c719aeab2baa 100644 --- a/src/content/docs/sandbox/api/backups.mdx +++ b/src/content/docs/sandbox/api/backups.mdx @@ -24,7 +24,7 @@ await sandbox.createBackup(options: BackupOptions): Promise - `options` - Backup configuration (see [`BackupOptions`](#backupoptions)): - `dir` (required) - Absolute path to the directory to back up (for example, `"/workspace"`) - `name` (optional) - Human-readable name for the backup. Maximum 256 characters, no control characters. - - `ttl` (optional) - Time-to-live in seconds until the backup expires. Default: `86400` (24 hours). Must be a positive number. + - `ttl` (optional) - Time-to-live in seconds until the backup expires. Default: `259200` (3 days). Must be a positive number. **Returns**: `Promise` containing: @@ -83,7 +83,7 @@ await sandbox.restoreBackup(backup: DirectoryBackup): Promise ``` @@ -194,7 +194,7 @@ interface BackupOptions { - `dir` (required) - Absolute path to the directory to back up - `name` (optional) - Human-readable backup name. Maximum 256 characters, no control characters. -- `ttl` (optional) - Time-to-live in seconds. Default: `86400` (24 hours). Must be a positive number. +- `ttl` (optional) - Time-to-live in seconds. Default: `259200` (3 days). Must be a positive number. ### `DirectoryBackup` @@ -216,7 +216,7 @@ interface DirectoryBackup { interface RestoreBackupResult { success: boolean; dir: string; - backupId: string; + id: string; } ``` @@ -224,7 +224,7 @@ interface RestoreBackupResult { - `success` - Whether the restore succeeded - `dir` - Directory that was restored -- `backupId` - Backup ID that was restored +- `id` - Backup ID that was restored ## Related resources diff --git a/src/content/docs/sandbox/guides/backup-restore.mdx b/src/content/docs/sandbox/guides/backup-restore.mdx index ec0a8a7f0b613f5..00ac7a1ad6626f2 100644 --- a/src/content/docs/sandbox/guides/backup-restore.mdx +++ b/src/content/docs/sandbox/guides/backup-restore.mdx @@ -194,7 +194,7 @@ console.log(`Backup ID: ${backup.id}`); ## Configure TTL -Set a custom time-to-live for backups. The default TTL is 24 hours (86400 seconds). The `ttl` value must be a positive number of seconds: +Set a custom time-to-live for backups. The default TTL is 3 days (259200 seconds). The `ttl` value must be a positive number of seconds: @@ -219,7 +219,7 @@ const longBackup = await sandbox.createBackup({ ### How TTL is enforced -The TTL is enforced at **restore time**, not at creation time. When you call `restoreBackup()`, the SDK reads the backup metadata from R2 and compares the creation timestamp plus TTL against the current time. If the TTL has elapsed, the restore is rejected with a `BACKUP_EXPIRED` error. +The TTL is enforced at **restore time**, not at creation time. When you call `restoreBackup()`, the SDK reads the backup metadata from R2 and compares the creation timestamp plus TTL against the current time (with a 60-second buffer to prevent race conditions). If the TTL has elapsed, the restore is rejected with a `BACKUP_EXPIRED` error. The TTL does **not** automatically delete backup objects from R2. Expired backups remain in your bucket and continue to consume storage until you explicitly delete them or configure an automatic cleanup rule. From e60ae05586de19e13a8be4a64ab414e49692054d Mon Sep 17 00:00:00 2001 From: katereznykova Date: Fri, 20 Feb 2026 18:14:55 +0000 Subject: [PATCH 05/10] chore: trigger rebuild From e3de2955b4c2be8ebdb9226e55b289c605c2d25c Mon Sep 17 00:00:00 2001 From: Jun Lee Date: Mon, 23 Feb 2026 15:31:30 +0000 Subject: [PATCH 06/10] Apply suggestions from code review --- src/content/docs/sandbox/api/backups.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/docs/sandbox/api/backups.mdx b/src/content/docs/sandbox/api/backups.mdx index 066c719aeab2baa..a9297a159191e43 100644 --- a/src/content/docs/sandbox/api/backups.mdx +++ b/src/content/docs/sandbox/api/backups.mdx @@ -32,7 +32,7 @@ await sandbox.createBackup(options: BackupOptions): Promise - `dir` - Directory that was backed up -``` +```ts import { getSandbox } from '@cloudflare/sandbox'; const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); @@ -86,7 +86,7 @@ await sandbox.restoreBackup(backup: DirectoryBackup): Promise -``` +```ts // Create a named backup with 24-hour TTL const backup = await sandbox.createBackup({ dir: '/workspace', From 1ff60acdf39d7603c6049d1fe2ab52f89bca5d8f Mon Sep 17 00:00:00 2001 From: Jun Lee Date: Mon, 23 Feb 2026 15:32:01 +0000 Subject: [PATCH 07/10] Update src/content/docs/sandbox/api/backups.mdx --- src/content/docs/sandbox/api/backups.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/sandbox/api/backups.mdx b/src/content/docs/sandbox/api/backups.mdx index a9297a159191e43..a4fa3cefe7797af 100644 --- a/src/content/docs/sandbox/api/backups.mdx +++ b/src/content/docs/sandbox/api/backups.mdx @@ -146,7 +146,7 @@ await sandbox.restoreBackup(checkpoint); ### Error handling -``` +```ts import { getSandbox } from "@cloudflare/sandbox"; From 0e70cacbd2fed57cb83acb808403fa25ad9e484e Mon Sep 17 00:00:00 2001 From: Jun Lee Date: Mon, 23 Feb 2026 15:32:21 +0000 Subject: [PATCH 08/10] Update src/content/docs/sandbox/api/backups.mdx --- src/content/docs/sandbox/api/backups.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/sandbox/api/backups.mdx b/src/content/docs/sandbox/api/backups.mdx index a4fa3cefe7797af..6db00a51fb191c3 100644 --- a/src/content/docs/sandbox/api/backups.mdx +++ b/src/content/docs/sandbox/api/backups.mdx @@ -127,7 +127,7 @@ The FUSE mount is lost when the sandbox sleeps or restarts. Re-restore from the Use backups as checkpoints before risky operations. -``` +```ts // Save checkpoint before risky operation const checkpoint = await sandbox.createBackup({ dir: '/workspace' }); From e82dd8b4afb6463ed0b430bf04dbaacbc15ca54b Mon Sep 17 00:00:00 2001 From: "ask-bonk[bot]" Date: Mon, 23 Feb 2026 15:38:27 +0000 Subject: [PATCH 09/10] Fix code formatting and number steps Co-authored-by: Oxyjun --- src/content/docs/sandbox/api/backups.mdx | 64 ++++---- .../docs/sandbox/guides/backup-restore.mdx | 140 +++++++++--------- 2 files changed, 104 insertions(+), 100 deletions(-) diff --git a/src/content/docs/sandbox/api/backups.mdx b/src/content/docs/sandbox/api/backups.mdx index 6db00a51fb191c3..6d1ead2590a1d60 100644 --- a/src/content/docs/sandbox/api/backups.mdx +++ b/src/content/docs/sandbox/api/backups.mdx @@ -32,18 +32,19 @@ await sandbox.createBackup(options: BackupOptions): Promise - `dir` - Directory that was backed up + ```ts -import { getSandbox } from '@cloudflare/sandbox'; +import { getSandbox } from "@cloudflare/sandbox"; -const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); +const sandbox = getSandbox(env.Sandbox, "my-sandbox"); // Create a backup of /workspace -const backup = await sandbox.createBackup({ dir: '/workspace' }); +const backup = await sandbox.createBackup({ dir: "/workspace" }); // Later, restore the backup await sandbox.restoreBackup(backup); +``` -```` **How it works**: @@ -54,6 +55,7 @@ await sandbox.restoreBackup(backup); 4. The local archive is cleaned up. **Throws**: + - `InvalidBackupConfigError` - If `dir` is not absolute, contains `..`, the `BACKUP_BUCKET` binding is missing, or the R2 presigned URL credentials are not configured - `BackupCreateError` - If the container fails to create the archive or the upload to R2 fails @@ -73,7 +75,7 @@ Restore a previously created backup into a directory using FUSE overlayfs (copy- ```ts await sandbox.restoreBackup(backup: DirectoryBackup): Promise -```` +``` **Parameters**: @@ -86,18 +88,19 @@ await sandbox.restoreBackup(backup: DirectoryBackup): Promise + ```ts // Create a named backup with 24-hour TTL const backup = await sandbox.createBackup({ - dir: '/workspace', - name: 'before-refactor', - ttl: 86400 + dir: "/workspace", + name: "before-refactor", + ttl: 86400, }); // Store the handle for later use await env.KV.put(`backup:${userId}`, JSON.stringify(backup)); - ``` + **How it works**: @@ -107,6 +110,7 @@ await env.KV.put(`backup:${userId}`, JSON.stringify(backup)); 3. The container mounts the squashfs archive with FUSE overlayfs. **Throws**: + - `InvalidBackupConfigError` - If `backup.id` is missing or not a valid UUID, or `backup.dir` is invalid - `BackupNotFoundError` - If the backup metadata or archive is not found in R2 - `BackupExpiredError` - If the backup TTL has elapsed @@ -127,43 +131,43 @@ The FUSE mount is lost when the sandbox sleeps or restarts. Re-restore from the Use backups as checkpoints before risky operations. -```ts +```ts // Save checkpoint before risky operation -const checkpoint = await sandbox.createBackup({ dir: '/workspace' }); +const checkpoint = await sandbox.createBackup({ dir: "/workspace" }); try { -await sandbox.exec('npm install some-experimental-package'); -await sandbox.exec('npm run build'); + await sandbox.exec("npm install some-experimental-package"); + await sandbox.exec("npm run build"); } catch (error) { -// Restore to the checkpoint if something goes wrong -await sandbox.restoreBackup(checkpoint); + // Restore to the checkpoint if something goes wrong + await sandbox.restoreBackup(checkpoint); } - ``` + ### Error handling -```ts +```ts import { getSandbox } from "@cloudflare/sandbox"; -const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); +const sandbox = getSandbox(env.Sandbox, "my-sandbox"); try { -const backup = await sandbox.createBackup({ dir: '/workspace' }); -console.log(`Backup created: ${backup.id}`); + const backup = await sandbox.createBackup({ dir: "/workspace" }); + console.log(`Backup created: ${backup.id}`); } 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); -} + 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); + } } +``` -```` ## Behavior @@ -184,11 +188,11 @@ The TTL does **not** automatically delete objects from R2. Expired backup archiv ```ts interface BackupOptions { - dir: string; - name?: string; - ttl?: number; + dir: string; + name?: string; + ttl?: number; } -```` +``` **Fields**: diff --git a/src/content/docs/sandbox/guides/backup-restore.mdx b/src/content/docs/sandbox/guides/backup-restore.mdx index 00ac7a1ad6626f2..9740807f4ae93b5 100644 --- a/src/content/docs/sandbox/guides/backup-restore.mdx +++ b/src/content/docs/sandbox/guides/backup-restore.mdx @@ -16,65 +16,65 @@ Backup and restore does not work with `wrangler dev` because it requires FUSE su ## Prerequisites -Create an R2 bucket for storing backups: - -```sh -npx wrangler r2 bucket create my-backup-bucket -``` - -Add the `BACKUP_BUCKET` R2 binding and presigned URL credentials to your Wrangler configuration: - - - -```jsonc -{ - "name": "my-sandbox-worker", - "main": "src/index.ts", - "compatibility_date": "$today", - "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": "", - }, - "r2_buckets": [ - { - "binding": "BACKUP_BUCKET", - "bucket_name": "my-backup-bucket", - }, - ], -} -``` - - - -Then set your R2 API credentials as secrets: - -```sh -npx wrangler secret put R2_ACCESS_KEY_ID -npx wrangler secret put R2_SECRET_ACCESS_KEY -``` - -You can create R2 API tokens in the [Cloudflare dashboard](https://dash.cloudflare.com/) under **R2** > **Overview** > **Manage R2 API Tokens**. The token needs **Object Read & Write** permissions for your backup bucket. +1. Create an R2 bucket for storing backups: + + ```sh + npx wrangler r2 bucket create my-backup-bucket + ``` + +2. Add the `BACKUP_BUCKET` R2 binding and presigned URL credentials to your Wrangler configuration: + + + + ```jsonc + { + "name": "my-sandbox-worker", + "main": "src/index.ts", + "compatibility_date": "$today", + "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": "", + }, + "r2_buckets": [ + { + "binding": "BACKUP_BUCKET", + "bucket_name": "my-backup-bucket", + }, + ], + } + ``` + + + +3. Set your R2 API credentials as secrets: + + ```sh + npx wrangler secret put R2_ACCESS_KEY_ID + npx wrangler secret put R2_SECRET_ACCESS_KEY + ``` + + You can create R2 API tokens in the [Cloudflare dashboard](https://dash.cloudflare.com/) under **R2** > **Overview** > **Manage R2 API Tokens**. The token needs **Object Read & Write** permissions for your backup bucket. ## Create a backup @@ -82,7 +82,7 @@ Use `createBackup()` to snapshot a directory and upload it to R2: -```typescript +```ts import { getSandbox } from "@cloudflare/sandbox"; const sandbox = getSandbox(env.Sandbox, "my-sandbox"); @@ -102,7 +102,7 @@ Use `restoreBackup()` to restore a directory from a backup: -```typescript +```ts import { getSandbox } from "@cloudflare/sandbox"; const sandbox = getSandbox(env.Sandbox, "my-sandbox"); @@ -127,7 +127,7 @@ Save state before risky operations and restore if something fails: -```typescript +```ts const sandbox = getSandbox(env.Sandbox, "my-sandbox"); // Save checkpoint before risky operation @@ -151,7 +151,7 @@ The `DirectoryBackup` handle is serializable. Persist it to KV, D1, or Durable O -```typescript +```ts const sandbox = getSandbox(env.Sandbox, "my-sandbox"); // Create a backup and store the handle in KV @@ -179,7 +179,7 @@ Add a `name` option to identify backups. Names can be up to 256 characters: -```typescript +```ts const sandbox = getSandbox(env.Sandbox, "my-sandbox"); const backup = await sandbox.createBackup({ @@ -198,7 +198,7 @@ Set a custom time-to-live for backups. The default TTL is 3 days (259200 seconds -```typescript +```ts const sandbox = getSandbox(env.Sandbox, "my-sandbox"); // Short-lived backup for a quick operation @@ -239,7 +239,7 @@ If you only need the most recent backup, delete the previous one before creating -```typescript +```ts import { getSandbox } from "@cloudflare/sandbox"; const sandbox = getSandbox(env.Sandbox, "my-sandbox"); @@ -268,7 +268,7 @@ To clean up multiple old backups, list objects under the `backups/` prefix and d -```typescript +```ts // List all backup objects in the bucket const listed = await env.BACKUP_BUCKET.list({ prefix: "backups/" }); @@ -295,7 +295,7 @@ If you have the backup ID, delete both its archive and metadata directly: -```typescript +```ts const backupId = backup.id; await env.BACKUP_BUCKET.delete(`backups/${backupId}/data.sqsh`); @@ -310,7 +310,7 @@ Restore uses FUSE overlayfs to mount the backup as a read-only lower layer. New -```typescript +```ts const sandbox = getSandbox(env.Sandbox, "my-sandbox"); // Create a backup @@ -337,7 +337,7 @@ Backup and restore operations can throw specific errors. Wrap calls in try/catch -```typescript +```ts import { getSandbox } from "@cloudflare/sandbox"; const sandbox = getSandbox(env.Sandbox, "my-sandbox"); From 283823bd384d7b804b59732d11ada3d3585ab133 Mon Sep 17 00:00:00 2001 From: katereznykova Date: Mon, 23 Feb 2026 16:02:58 +0000 Subject: [PATCH 10/10] chore: re-trigger CI