diff --git a/src/content/docs/sandbox/api/backups.mdx b/src/content/docs/sandbox/api/backups.mdx new file mode 100644 index 000000000000000..6d1ead2590a1d60 --- /dev/null +++ b/src/content/docs/sandbox/api/backups.mdx @@ -0,0 +1,237 @@ +--- +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: `259200` (3 days). Must be a positive number. + +**Returns**: `Promise` containing: + +- `id` - Unique backup identifier (UUID) +- `dir` - Directory that was backed up + + + +```ts +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 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 `..`, 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 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] +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 +- `id` - Backup ID that was restored + + + +```ts +// 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. 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 +- `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. + + + +```ts +// 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 + + + +```ts +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 + +- 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` + +```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: `259200` (3 days). Must be a positive number. + +### `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; + id: string; +} +``` + +**Fields**: + +- `success` - Whether the restore succeeded +- `dir` - Directory that was restored +- `id` - 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..32a4cf0ec333814 100644 --- a/src/content/docs/sandbox/configuration/wrangler.mdx +++ b/src/content/docs/sandbox/configuration/wrangler.mdx @@ -52,6 +52,46 @@ 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/), 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", + "bucket_name": "my-backup-bucket", + }, + ], +} +``` + + +### 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 ### 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..9740807f4ae93b5 --- /dev/null +++ b/src/content/docs/sandbox/guides/backup-restore.mdx @@ -0,0 +1,390 @@ +--- +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 + +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 + +Use `createBackup()` to snapshot a directory and upload it to R2: + + + +```ts +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 directly to your R2 bucket using a presigned URL. + +## Restore a backup + +Use `restoreBackup()` to restore a directory from a backup: + + + +```ts +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: + + + +```ts +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: + + + +```ts +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); +} +``` + + + +## Use named backups + +Add a `name` option to identify backups. Names can be up to 256 characters: + + + +```ts +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 3 days (259200 seconds). The `ttl` value must be a positive number of seconds: + + + +```ts +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 +}); +``` + + + +### 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 (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. + +### 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: + + + +```ts +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: + + + +```ts +// 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: + + + +```ts +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 + +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: + + + +```ts +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: + + + +```ts +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); + } +} +``` + + + +## 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 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 +- **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