Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/content/docs/sandbox/api/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ The Sandbox SDK provides a comprehensive API for executing code, managing files,
and APIs from the internet.
</LinkTitleCard>

<LinkTitleCard title="Tunnels" href="/sandbox/api/tunnels/" icon="connection">
Expose services on zero-config `*.trycloudflare.com` URLs via
`sandbox.tunnels.get(port)`. Best for quick development and `.workers.dev`
deployments.
</LinkTitleCard>

<LinkTitleCard title="Storage" href="/sandbox/api/storage/" icon="database">
Mount S3-compatible buckets (R2, S3, GCS) as local filesystems for persistent
data storage across sandbox lifecycles.
Expand Down
6 changes: 6 additions & 0 deletions src/content/docs/sandbox/api/ports.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { TypeScriptExample } from "~/components";
Preview URLs require a custom domain with wildcard DNS routing in production. See [Production Deployment](/sandbox/guides/production-deployment/).
:::

:::note[Alternative: quick tunnels]
For quick development or `.workers.dev` deployments, consider [`sandbox.tunnels`](/sandbox/api/tunnels/), which returns `*.trycloudflare.com` URLs without DNS setup. `exposePort()` remains the recommended option for production.
:::

Expose services running in your sandbox via public preview URLs. See [Preview URLs concept](/sandbox/concepts/preview-urls/) for details.

## Module functions
Expand Down Expand Up @@ -285,3 +289,5 @@ export default {
- [Expose Services guide](/sandbox/guides/expose-services/) - Full workflow for starting services, exposing ports, and routing requests
- [WebSocket Connections guide](/sandbox/guides/websocket-connections/) - WebSocket routing via preview URLs
- [Commands API](/sandbox/api/commands/) - Start background processes
- [Tunnels API](/sandbox/api/tunnels/) - Zero-config `*.trycloudflare.com` URLs for quick development
```
151 changes: 151 additions & 0 deletions src/content/docs/sandbox/api/tunnels.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
title: Tunnels
description: Expose sandbox services on zero-config *.trycloudflare.com URLs using the Sandbox SDK tunnels API.
pcx_content_type: reference
sidebar:
order: 6
products:
- sandbox
---

import { TypeScriptExample } from "~/components";

The `sandbox.tunnels` namespace exposes a service running inside a sandbox on a `*.trycloudflare.com` URL. The SDK runs `cloudflared` inside the container and opens a persistent QUIC connection to Cloudflare's edge; no Cloudflare account, DNS record, or custom domain is required.

:::note[When to use tunnels vs. `exposePort()`]
Use `sandbox.tunnels` for **quick development** and **deployments on `.workers.dev`**, where wildcard DNS is not available. For **production**, use [`exposePort()`](/sandbox/api/ports/) with a custom domain — quick tunnels are positioned by Cloudflare as a debug aid and do not carry an uptime guarantee. Production-grade named tunnels are planned for a future release.
:::

## Requirements

- **RPC transport.** Calling `sandbox.tunnels` on HTTP/Websocket transports will throw `"RPC transport required"`. See [Transport configuration](/sandbox/configuration/transport/).
- **glibc image variant.** The default, `python`, `opencode`, and `desktop` images ship `cloudflared`. The `musl`/Alpine variant does not — there is no upstream `cloudflared` build for musl at this time.

## Methods

### `tunnels.get()`

Return a tunnel record for `port`. The SDK spawns a fresh `cloudflared` process inside the container if not already running. The method is idempotent so repeated calls for the same port return the same record.

```ts
const tunnel = await sandbox.tunnels.get(port: number): Promise<TunnelInfo>
```

**Parameters**:

- `port` — Port number inside the sandbox to expose (1024-65535, excluding reserved ports). The service to tunnel to must already be listening on `0.0.0.0:<port>` inside the container.

**Returns**: `Promise<TunnelInfo>` — the tunnel record. See [`TunnelInfo`](#tunnelinfo).

<TypeScriptExample>
```ts
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
async fetch(request: Request, env: Env): Promise<Response> {
const sandbox = getSandbox(env.Sandbox, "my-sandbox");

await sandbox.startProcess("python -m http.server 8080");

const tunnel = await sandbox.tunnels.get(8080);
console.log(tunnel.url);
// → https://random-words-here.trycloudflare.com

// Repeated calls for the same port return the same record.
const same = await sandbox.tunnels.get(8080);
console.log(same.url === tunnel.url); // true

return Response.json({ url: tunnel.url });

},
};

````
</TypeScriptExample>

### `tunnels.list()`

Return every tunnel currently tracked for this sandbox.

```ts
const tunnels = await sandbox.tunnels.list(): Promise<TunnelInfo[]>
````

**Returns**: `Promise<TunnelInfo[]>` — an array of [`TunnelInfo`](#tunnelinfo) records. Empty when no tunnels are active.

<TypeScriptExample>
```ts
const tunnels = await sandbox.tunnels.list();

for (const tunnel of tunnels) {
console.log(`port ${tunnel.port} → ${tunnel.url}`);
}

````
</TypeScriptExample>

### `tunnels.destroy()`

Tear down a tunnel. Accepts either the port number or the `TunnelInfo` record returned by `get()`. Idempotent — destroying an unknown port resolves successfully.

```ts
await sandbox.tunnels.destroy(portOrInfo: number | TunnelInfo): Promise<void>
````

**Parameters**:

- `portOrInfo` — Either the port number or the `TunnelInfo` record returned by [`get()`](#tunnelsget).

<TypeScriptExample>
```ts
const tunnel = await sandbox.tunnels.get(8080);

// Tear down by port number...
await sandbox.tunnels.destroy(8080);

// ...or by the record.
await sandbox.tunnels.destroy(tunnel);

````
</TypeScriptExample>

## Types

### `TunnelInfo`

| Field | Type | Description |
| ----------- | -------- | -------------------------------------------------------------------------------------------- |
| `id` | `string` | SDK-assigned identifier for the tunnel (for example, `quick-9f2c8a1d`). |
| `port` | `number` | Port number inside the sandbox that the tunnel proxies to. |
| `url` | `string` | Public URL — `https://<random-words>.trycloudflare.com`. |
| `hostname` | `string` | Hostname component of `url` (`<random-words>.trycloudflare.com`). |
| `createdAt` | `string` | ISO-8601 timestamp of when the tunnel was created. |

```ts
interface TunnelInfo {
id: string;
port: number;
url: string;
hostname: string;
createdAt: string;
}
````

## Limitations

- **URLs do not survive container restart.** Cloudflare assigns the hostname during `cloudflared`'s startup handshake, so every restart yields a new URL. The SDK clears its tunnel cache when the container starts, so the next `tunnels.get(port)` returns a fresh record.
- **No uptime guarantee.** Cloudflare positions `trycloudflare.com` as a debug aid, not a production target. Use [`exposePort()`](/sandbox/api/ports/) with a custom domain for production.
- **No Server-Sent Events.** The `trycloudflare.com` edge buffers `text/event-stream` responses, so SSE does not reach the client. WebSockets work normally.
- **No persistent hostname.** Every restart picks a new `<random-words>.trycloudflare.com`. If you need a stable URL, use [`exposePort()`](/sandbox/api/ports/#exposeport) with a custom token.
- **Brief DNS warm-up.** The first request through a brand-new URL can take a couple of seconds while DNS propagates, even after `get()` resolves.
- **WARP / Zero Trust egress.** If your local machine runs Cloudflare WARP or another Zero Trust egress policy, outbound traffic to `api.trycloudflare.com` and the cloudflared edge can be blocked. When that happens, `tunnels.get()` hangs on the edge handshake and eventually times out. Disable WARP or add an egress exception for these destinations.
- **No musl/Alpine support.** The musl image variant does not include `cloudflared`. Use one of the glibc-based image variants (`default`, `python`, `opencode`, `desktop`).

## Related resources

- [Preview URLs concept](/sandbox/concepts/preview-urls/) — Worker-fronted preview URLs and how they differ from quick tunnels.
- [Ports API](/sandbox/api/ports/) — `exposePort()` and the Worker-fronted preview URL flow.
- [Expose services guide](/sandbox/guides/expose-services/) — End-to-end walkthrough for exposing services in production.
- [Transport configuration](/sandbox/configuration/transport/) — RPC vs. route-based transport.
2 changes: 1 addition & 1 deletion src/content/docs/sandbox/concepts/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ These pages explain how the Sandbox SDK works, why it's designed the way it is,
- [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - Understanding sandbox states and behavior
- [Container runtime](/sandbox/concepts/containers/) - How code executes in isolated containers
- [Session management](/sandbox/concepts/sessions/) - When and how to use sessions
- [Preview URLs](/sandbox/concepts/preview-urls/) - How service exposure works
- [Preview URLs](/sandbox/concepts/preview-urls/) - How to expose sandboxed services on the public internet.
- [Security model](/sandbox/concepts/security/) - Isolation, validation, and safety mechanisms
- [Terminal connections](/sandbox/concepts/terminal/) - How browser terminal connections work

Expand Down
47 changes: 40 additions & 7 deletions src/content/docs/sandbox/concepts/preview-urls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,36 @@ products:
- sandbox
---

# Quick deployment

For quick preview deployments we recommend using [Cloudflare Tunnel](https://developers.cloudflare.com/tunnel/) to generate preview URLs to your web services. These work across local development, workers.dev and production usage.

```ts
await sandbox.startProcess("python -m http.server 8000");
const tunnel = await sandbox.tunnels.get(8000);
console.log(tunnel.url);
// https://acute-llama-dancing-roundly.trycloudflare.app

// Request will be routed directly to the webserver running on the sandbox.
const req = await fetch(`${tunnel.url}/api/users`); // => GET http://localhost:8000/api/users
```

Cloudflare Tunnel support currently has the following limitations:

- No control over generated URL.
- No authentication mechanism beyond randomly generated URL.
- Each URL uses an additional `cloudflared` process on the sandbox.

:::note[Production requires custom domain]
We are working on production deployments, custom hostnames and authentication for Cloudflare Tunnel support. In the mean time we recommend using `exposePort()` and `proxyToSandbox()` documented below under [Production usage, stable URLs and custom domains](#).
:::

See the [tunnels API reference](/sandbox/api/tunnels/) for the full API and feature set.

# Production usage, stable URLs & custom domains

For production use we recommend using the `exposePort()` API and routing traffic through your worker.

:::note[Production requires custom domain]
Preview URLs work in local development without configuration. For production, you need a custom domain with wildcard DNS routing. See [Production Deployment](/sandbox/guides/production-deployment/).
:::
Expand Down Expand Up @@ -53,20 +83,22 @@ URLs with auto-generated tokens change when you unexpose and re-expose a port.
For production deployments or shared URLs, specify a custom token to maintain consistency across container restarts:

```typescript
const stable = await sandbox.exposePort(8000, {
hostname,
token: 'api_v1'
const stable = await sandbox.exposePort(8000, {
hostname,
token: "api_v1",
});
// https://8000-sandbox-id-api_v1.yourdomain.com
// Same URL every time ✓
```

**Token requirements:**

- 1-16 characters long
- Lowercase letters (a-z), numbers (0-9), and underscores (_) only
- Lowercase letters (a-z), numbers (0-9), and underscores (\_) only
- Must be unique within each sandbox

**Use cases for custom tokens:**

- Production APIs with stable endpoints
- Sharing demo URLs with external users
- Documentation with consistent examples
Expand All @@ -80,7 +112,7 @@ Preview URLs extract the sandbox ID from the hostname to route requests. Since h

```typescript
// Problem scenario
const sandbox = getSandbox(env.Sandbox, 'MyProject-123');
const sandbox = getSandbox(env.Sandbox, "MyProject-123");
// Durable Object ID: "MyProject-123"
await sandbox.exposePort(8080, { hostname });
// Preview URL: 8080-myproject-123-token123.yourdomain.com
Expand All @@ -90,8 +122,8 @@ await sandbox.exposePort(8080, { hostname });
**The solution**: Use `normalizeId: true` to lowercase IDs when creating sandboxes:

```typescript
const sandbox = getSandbox(env.Sandbox, 'MyProject-123', {
normalizeId: true
const sandbox = getSandbox(env.Sandbox, "MyProject-123", {
normalizeId: true,
});
// Durable Object ID: "myproject-123" (lowercased)
// Preview URL: 8080-myproject-123-token123.yourdomain.com
Expand Down Expand Up @@ -266,4 +298,5 @@ This is **only required for local development**. In production, all container po
- [Production Deployment](/sandbox/guides/production-deployment/) - Set up custom domains for production
- [Expose Services](/sandbox/guides/expose-services/) - Practical patterns for exposing ports
- [Ports API](/sandbox/api/ports/) - Complete API reference
- [Tunnels API](/sandbox/api/tunnels/) - Zero-config `*.trycloudflare.com` URLs as an alternative for development
- [Security Model](/sandbox/concepts/security/) - Security best practices
10 changes: 10 additions & 0 deletions src/content/docs/sandbox/concepts/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ To revoke access, unexpose the port:
await sandbox.unexposePort(8080);
```

### Quick tunnel URLs

Quick tunnels (`sandbox.tunnels.get(port)`) return a `*.trycloudflare.com` URL with a random hostname assigned by Cloudflare — there is no separate access token. The hostname itself is the access control: anyone who knows the URL can reach the service. To revoke access, destroy the tunnel:

```typescript
await sandbox.tunnels.destroy(8080);
```

URLs do not survive a container restart, so a restart effectively rotates the hostname. As with preview URLs, add application-level authentication for any sensitive service. See the [Tunnels API](/sandbox/api/tunnels/) for details.

```python
from flask import Flask, request, abort
import os
Expand Down
5 changes: 1 addition & 4 deletions src/content/docs/sandbox/get-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,6 @@ curl https://my-sandbox.YOUR_SUBDOMAIN.workers.dev/run

Your sandbox is now deployed and can execute code in isolated containers.

:::note[Preview URLs require custom domain]
If you plan to expose ports from sandboxes (using `exposePort()` for preview URLs), you will need to set up a custom domain with wildcard DNS routing. The `.workers.dev` domain does not support the subdomain patterns required for preview URLs. See [Production Deployment](/sandbox/guides/production-deployment/) when you are ready to expose services.
:::

## Understanding the configuration

Your `wrangler.jsonc` connects three pieces together:
Expand Down Expand Up @@ -210,5 +206,6 @@ Now that you have a working sandbox, explore more capabilities:
- [Execute commands](/sandbox/guides/execute-commands/) - Run shell commands and stream output
- [Manage files](/sandbox/guides/manage-files/) - Work with files and directories
- [Expose services](/sandbox/guides/expose-services/) - Get public URLs for services running in your sandbox
- [Quick tunnels](/sandbox/api/tunnels/) - Zero-config `*.trycloudflare.com` URLs for development and `.workers.dev` deployments
- [Production Deployment](/sandbox/guides/production-deployment/) - Set up custom domains for preview URLs
- [API reference](/sandbox/api/) - Complete API documentation
5 changes: 5 additions & 0 deletions src/content/docs/sandbox/guides/expose-services.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { TypeScriptExample } from "~/components";
Preview URLs require a custom domain with wildcard DNS routing in production. See [Production Deployment](/sandbox/guides/production-deployment/) for setup instructions.
:::

:::note[Alternative: quick tunnels]
If you only need a public URL for development or a `.workers.dev` deployment, [`sandbox.tunnels`](/sandbox/api/tunnels/) returns a `*.trycloudflare.com` URL without DNS setup. For production, follow this guide and use `exposePort()` with a custom domain.
:::

This guide shows you how to expose services running in your sandbox to the internet via preview URLs.

## When to expose ports
Expand Down Expand Up @@ -393,3 +397,4 @@ See [Sandbox options - normalizeId](/sandbox/configuration/sandbox-options/#norm
- [Ports API reference](/sandbox/api/ports/) - Complete port exposure API
- [Background processes guide](/sandbox/guides/background-processes/) - Managing services
- [Execute commands guide](/sandbox/guides/execute-commands/) - Starting services
- [Tunnels API reference](/sandbox/api/tunnels/) - Quick `*.trycloudflare.com` URLs as an alternative to `exposePort()`
5 changes: 4 additions & 1 deletion src/content/docs/sandbox/guides/production-deployment.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ products:
import { WranglerConfig } from "~/components";

:::note[Only required for preview URLs]
Custom domain setup is ONLY needed if you use `exposePort()` to expose services from sandboxes. If your application does not expose ports, you can deploy to `.workers.dev` without this configuration.
This guide covers `exposePort()` in production. Custom domain setup is ONLY needed if you use `exposePort()` to expose services from sandboxes. If your application does not use `exposePort()`, you can deploy to `.workers.dev` without this configuration.

For development or `.workers.dev` deployments that need public URLs, [`sandbox.tunnels`](/sandbox/api/tunnels/) is a zero-config alternative — we recommend `exposePort()` for production today; named tunnels for production are planned for a future release.
:::

Deploy your Sandbox SDK application to production with preview URL support. Preview URLs require wildcard DNS routing because they generate unique subdomains for each exposed port: `https://8080-abc123.yourdomain.com`.
Expand Down Expand Up @@ -108,5 +110,6 @@ For detailed troubleshooting, see the [Workers routing documentation](/workers/c

- [Preview URLs](/sandbox/concepts/preview-urls/) - How preview URLs work
- [Expose Services](/sandbox/guides/expose-services/) - Patterns for exposing ports
- [Tunnels API](/sandbox/api/tunnels/) - Zero-config `*.trycloudflare.com` URLs for development (not for production)
- [Workers Routing](/workers/configuration/routing/) - Advanced routing configuration
- [Cloudflare DNS](/dns/) - DNS management
5 changes: 5 additions & 0 deletions src/content/docs/sandbox/guides/websocket-connections.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ export default {
```
</TypeScriptExample>

:::note[Alternative: quick tunnels]
Quick tunnels also handle WebSocket upgrades and do not require a custom domain, so they work on `.workers.dev`. Swap `sandbox.exposePort(8080, { hostname })` for `sandbox.tunnels.get(8080)` to get a `*.trycloudflare.com` URL.
:::

**Client connects to preview URL:**

```javascript
Expand Down Expand Up @@ -246,4 +250,5 @@ Port exposure in Dockerfile is only required for local development. In productio

- [Ports API reference](/sandbox/api/ports/) - Complete API documentation
- [Preview URLs concept](/sandbox/concepts/preview-urls/) - How preview URLs work
- [Tunnels API](/sandbox/api/tunnels/) - Zero-config `*.trycloudflare.com` URLs for WebSocket services in development
- [Background processes guide](/sandbox/guides/background-processes/) - Managing long-running services