Skip to content
Closed
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
48 changes: 39 additions & 9 deletions src/content/docs/sandbox/concepts/preview-urls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,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), hyphens (-), and underscores (_) only
- Lowercase letters (a-z), numbers (0-9), hyphens (-), 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 @@ -77,7 +79,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 @@ -87,8 +89,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 @@ -120,7 +122,9 @@ export default {
};
```

Requests flow: Browser → Your Worker → Durable Object (sandbox) → Your Service.
**Request flow**: Browser → HTTPS → Cloudflare edge (TLS termination) → Worker → Durable Object → HTTP → Container

Cloudflare terminates TLS at the edge. Your service inside the container receives plain HTTP requests over an internal connection -- no TLS is needed because the transport between the Durable Object and the container VM is already secure. Your service should bind on HTTP, not HTTPS.

## Multiple Ports

Expand Down Expand Up @@ -155,6 +159,7 @@ const admin = await sandbox.exposePort(3001, { hostname, name: "admin" });
- Custom protocols (must wrap in HTTP)
- Ports outside range 1024-65535
- Port 3000 (used internally by the SDK)
- HTTPS servers inside the container (the internal connection uses HTTP -- Cloudflare handles TLS at the edge, so your service should serve plain HTTP on the exposed port)

## WebSocket Support

Expand Down Expand Up @@ -191,7 +196,7 @@ Preview URLs are publicly accessible by default, but require a valid access toke
**Built-in security**:

- **Token-based access** - Each exposed port gets a unique token in the URL (for example, `https://8080-sandbox-abc123token456.yourdomain.com`)
- **HTTPS in production** - All traffic is encrypted with TLS. Certificates are provisioned automatically for first-level wildcards (`*.yourdomain.com`). If your worker runs on a subdomain, see the [TLS note in Production Deployment](/sandbox/guides/production-deployment/).
- **HTTPS in production** - All traffic between the browser and Cloudflare is encrypted with TLS. If your Worker runs on the apex domain, Cloudflare's Universal SSL covers `*.yourdomain.com` automatically. If your Worker runs on a subdomain (for example, `sandbox.yourdomain.com`), you need a certificate covering `*.sandbox.yourdomain.com` -- Universal SSL does not cover second-level wildcards. Refer to [Production Deployment](/sandbox/guides/production-deployment/) for options including [Advanced Certificate Manager](/ssl/edge-certificates/advanced-certificate-manager/).
- **Unpredictable URLs** - Auto-generated tokens are randomly generated and difficult to guess
- **Token collision prevention** - Custom tokens are validated to ensure uniqueness within each sandbox

Expand Down Expand Up @@ -236,6 +241,31 @@ app.run((host = "0.0.0.0"), (port = 3000));
app.run((host = "127.0.0.1"), (port = 3000));
```

### Container HTTPS Error

If you receive an error when connecting to your container, your service inside the sandbox may be serving HTTPS. Configure it to serve HTTP instead. Cloudflare handles TLS between the browser and the edge -- the connection from the Durable Object to your container is internal and already secure.

Common causes:

- Node.js using `https.createServer()` instead of `http.createServer()`
- A framework started with an HTTPS flag (for example, `--experimental-https` in Next.js)
- A framework redirecting HTTP to HTTPS (configure it to trust the proxy -- for example, `app.set('trust proxy', 1)` in Express)
- Auth middleware or framework middleware producing HTTPS redirect URLs. `proxyToSandbox()` sets `X-Forwarded-Proto: https` on requests to your container. Frameworks like Next.js read this header and construct internal URLs with `https://`. If your middleware redirects using these URLs, the `Location` header may contain an absolute `https://` URL. The container binding follows redirects by default and rejects the `https://` scheme. To fix, use relative paths in redirects, or set your auth library's base URL to the container's internal HTTP address (for example, `NEXTAUTH_URL=http://localhost:3001`).

#### Next.js

Next.js reads the `X-Forwarded-Proto` header and uses it to construct internal URLs. When running `next dev` behind a Sandbox preview URL:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bonk please substantiate this claim by researching the Next.js codebase (https://github.com/vercel/next.js) and linking to relevant context


- Middleware receives `request.url` with `https://` protocol
- Auth libraries (NextAuth, Auth.js, Clerk) may construct redirect URLs using this protocol
- Server Actions that call `redirect()` trigger an internal self-fetch using the HTTPS protocol

To fix:

- Set your auth library's base URL to the container's internal address: `NEXTAUTH_URL=http://localhost:3001` or `AUTH_URL=http://localhost:3001`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bonk this should be clear that you should do this only in the context of local dev within the sandbox, in a context where TLS is handled by the proxy in front

- If using Server Actions with redirects, set `__NEXT_PRIVATE_ORIGIN=http://localhost:3001` as an environment variable in the container
- Use relative paths in middleware redirects instead of constructing URLs from `request.url`

### Production Errors

For custom domain issues, see [Production Deployment troubleshooting](/sandbox/guides/production-deployment/#troubleshooting).
Expand Down
120 changes: 107 additions & 13 deletions src/content/docs/sandbox/guides/production-deployment.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,58 @@ The `.workers.dev` domain does not support wildcard subdomains, so production de
If your worker runs on a subdomain (for example, `sandbox.yourdomain.com`), preview URLs become second-level wildcards like `*.sandbox.yourdomain.com`. Cloudflare's Universal SSL only covers first-level wildcards (`*.yourdomain.com`), so you need a certificate covering `*.sandbox.yourdomain.com`. Without it, preview URLs will fail with TLS handshake errors.

You have three options:

- **Deploy on the apex domain** (`yourdomain.com`) so preview URLs stay at the first level (`*.yourdomain.com`), which Universal SSL covers automatically. This is the simplest option.
- **Use [Advanced Certificate Manager](/ssl/edge-certificates/advanced-certificate-manager/)** ($10/month) to provision a certificate for `*.sandbox.yourdomain.com` through the Cloudflare dashboard.
- **Upload a custom certificate** from a provider like [Let's Encrypt](https://letsencrypt.org/) (free). Generate a wildcard certificate for `*.sandbox.yourdomain.com` using the DNS-01 challenge, then upload it via the Cloudflare dashboard under **SSL/TLS > Edge Certificates > [Custom Certificates](/ssl/edge-certificates/custom-certificates/)**. You will need to renew it before expiry.
:::
- **Use [Advanced Certificate Manager](/ssl/edge-certificates/advanced-certificate-manager/)** to provision a certificate for `*.sandbox.yourdomain.com` through the Cloudflare dashboard.
- **Upload a custom certificate** from a provider like [Let's Encrypt](https://letsencrypt.org/) (free). Generate a wildcard certificate for `*.sandbox.yourdomain.com` using the DNS-01 challenge, then upload it via the Cloudflare dashboard under **SSL/TLS** > **Edge Certificates** > **[Custom Certificates](/ssl/edge-certificates/custom-certificates/)**. You will need to renew it before expiry.
:::

## How preview URL requests flow

When a browser visits a preview URL like `https://8080-my-sandbox-abc123.yourdomain.com`:

1. **DNS**: The wildcard record (`*.yourdomain.com`) resolves the hostname to Cloudflare's proxy.
2. **TLS**: Cloudflare terminates TLS at the edge using your domain's certificate. The browser gets a secure HTTPS connection.
3. **Worker route**: The wildcard route (`*.yourdomain.com/*`) matches the request and sends it to your Worker.
4. **`proxyToSandbox()`**: Your Worker calls `proxyToSandbox()`, which extracts the sandbox ID and port from the hostname, rewrites the URL to `http://localhost:{port}`, and forwards the request to the correct Durable Object. It sets `X-Forwarded-Proto: https` so your application can detect the original protocol.
5. **Container**: The Durable Object forwards the request to the service inside your container over an internal connection (HTTP -- TLS is not needed because the transport is already secure within Cloudflare's network). If your service responds with an HTTP redirect, the container binding follows it automatically. If the redirect `Location` header contains an `https://` URL, the request fails. To prevent this, ensure your application produces HTTP or relative redirect URLs, or pass `redirect: "manual"` in your Worker to handle redirects yourself.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/bonk can you create a mermaid diagram of this request flow based on the description in this section?

Please don't yet add it to the docs but just leave it here as a comment and put it in a markdown block to render


```mermaid
sequenceDiagram
participant Browser
participant DNS
participant Edge as Cloudflare Edge (TLS)
participant Route as Worker Route
participant Worker as Worker (proxyToSandbox)
participant DO as Durable Object
participant Container as Container (HTTP)

Browser->>DNS: Resolve 8080-my-sandbox-abc123.yourdomain.com
DNS-->>Browser: *.yourdomain.com → Cloudflare proxy IP

Browser->>Edge: HTTPS request
Edge->>Edge: Terminate TLS using domain certificate
Edge-->>Browser: Secure HTTPS connection established

Edge->>Route: Forward request
Route->>Route: Match *.yourdomain.com/*
Route->>Worker: Dispatch to Worker

Worker->>Worker: Extract sandbox ID & port from hostname
Worker->>Worker: Rewrite URL to http://localhost:{port}
Worker->>Worker: Set X-Forwarded-Proto: https
Worker->>DO: Forward request to Durable Object

DO->>Container: HTTP request (internal, already secure)
Container-->>DO: HTTP response
DO-->>Worker: Response
Worker-->>Edge: Response
Edge-->>Browser: HTTPS response

Note over DO,Container: If container responds with 302 Location: https://...<br/>the binding follows the redirect and fails.<br/>Use redirect: "manual" to debug.
```

Each step can fail independently: DNS can resolve but TLS can fail (certificate does not cover the hostname), or TLS can succeed but the route does not match (request reaches the wrong Worker or no Worker at all).

## Prerequisites

Expand All @@ -35,18 +83,39 @@ You have three options:

### Create Wildcard DNS Record

In the Cloudflare dashboard, go to your domain and create an A record:
In the Cloudflare dashboard, go to your domain and create an A record. The record name depends on whether your Worker runs on the apex domain or a subdomain.

#### Apex domain

If your Worker runs on the apex domain (`yourdomain.com`), create a wildcard record for `*`:

- **Type**: A
- **Name**: * (wildcard)
- **IPv4 address**: 192.0.2.0
- **Name**: `*` (wildcard)
- **IPv4 address**: `192.0.2.0`
- **Proxy status**: Proxied (orange cloud)

This routes all subdomains through Cloudflare's proxy. The IP address `192.0.2.0` is a documentation address (RFC 5737) that Cloudflare recognizes when proxied.
This routes all subdomains through Cloudflare's proxy. The IP address `192.0.2.0` is a documentation address (RFC 5737) that Cloudflare recognizes when proxied. Preview URLs will look like `https://8080-sandbox-id-token.yourdomain.com`.

#### Subdomain

If your Worker runs on a subdomain (for example, `sandbox.yourdomain.com`), create a wildcard record for `*.sandbox`:

- **Type**: A
- **Name**: `*.sandbox` (where `sandbox` matches your Worker's subdomain)
- **IPv4 address**: `192.0.2.0`
- **Proxy status**: Proxied (orange cloud)

This routes all sub-subdomains of `sandbox.yourdomain.com` through Cloudflare's proxy. Preview URLs will look like `https://8080-sandbox-id-token.sandbox.yourdomain.com`.

:::caution[TLS certificate required]
You also need a TLS certificate covering `*.sandbox.yourdomain.com`. Universal SSL does not cover second-level wildcards. Refer to the [TLS caution at the top of this page](#subdomain-depth-matters-for-tls), or use [Advanced Certificate Manager](/ssl/edge-certificates/advanced-certificate-manager/) to order one through the dashboard.
:::

### Configure Worker Routes

Add a wildcard route to your Wrangler configuration:
Add a wildcard route to your Wrangler configuration. The route pattern must match the domain or subdomain your Worker runs on.

#### Apex domain

<WranglerConfig>
```jsonc
Expand All @@ -65,7 +134,28 @@ Add a wildcard route to your Wrangler configuration:
```
</WranglerConfig>

Replace `yourdomain.com` with your actual domain. This routes all subdomain requests to your Worker and enables Cloudflare to provision SSL certificates automatically.
Replace `yourdomain.com` with your actual domain.

#### Subdomain

<WranglerConfig>
```jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-sandbox-app",
"main": "src/index.ts",
"compatibility_date": "$today",
"routes": [
{
"pattern": "*.sandbox.yourdomain.com/*",
"zone_name": "yourdomain.com"
}
]
}
```
</WranglerConfig>

Replace `sandbox.yourdomain.com` with your actual subdomain and `yourdomain.com` with your zone name.

### Deploy

Expand All @@ -83,11 +173,11 @@ Test that preview URLs work:
// Extract hostname from request
const { hostname } = new URL(request.url);

const sandbox = getSandbox(env.Sandbox, 'test-sandbox');
await sandbox.startProcess('python -m http.server 8080');
const sandbox = getSandbox(env.Sandbox, "test-sandbox");
await sandbox.startProcess("python -m http.server 8080");
const exposed = await sandbox.exposePort(8080, { hostname });

console.log(exposed.exposedAt);
console.log(exposed.url);
// https://8080-test-sandbox.yourdomain.com
```

Expand All @@ -96,9 +186,13 @@ Visit the URL in your browser to confirm your service is accessible.
## Troubleshooting

- **CustomDomainRequiredError**: Verify your Worker is not deployed to `.workers.dev` and that the wildcard DNS record and route are configured correctly.
- **SSL/TLS errors**: Wait a few minutes for certificate provisioning. Verify the DNS record is proxied and SSL/TLS mode is set to "Full" or "Full (strict)" in your dashboard. If your worker is on a subdomain (for example, `sandbox.yourdomain.com`), Universal SSL won't cover the second-level wildcard `*.sandbox.yourdomain.com` see the [TLS caution](#subdomain-depth-matters-for-tls) at the top of this page for options.
- **SSL/TLS errors**: Wait a few minutes for certificate provisioning. Verify the DNS record is proxied and SSL/TLS mode is set to "Full" or "Full (strict)" in your dashboard. If your worker is on a subdomain (for example, `sandbox.yourdomain.com`), Universal SSL will not cover the second-level wildcard `*.sandbox.yourdomain.com` -- see the [TLS caution](#subdomain-depth-matters-for-tls) at the top of this page for options.
- **Preview URL not resolving**: Confirm the wildcard DNS record exists and is proxied. Wait 30-60 seconds for DNS propagation.
- **Port not accessible**: Ensure your service binds to `0.0.0.0` (not `localhost`) and that `proxyToSandbox()` is called first in your Worker's fetch handler.
- **Container HTTPS error**: Your service inside the sandbox is using HTTPS (for example, Node.js `https.createServer()` or a framework with `--https` enabled). Configure it to serve plain HTTP instead. Cloudflare terminates TLS at the edge, so the browser connects over HTTPS, but the internal connection to your container uses HTTP. If your framework checks the protocol and redirects HTTP to HTTPS, configure it to trust the proxy (for example, `app.set('trust proxy', 1)` in Express). This error also occurs when your application responds with an HTTP redirect to an `https://` URL. The container binding follows redirects by default. If your framework middleware or auth library constructs redirect URLs using the `X-Forwarded-Proto` header (which `proxyToSandbox()` sets to `https`), the `Location` header may contain an absolute `https://` URL that fails when followed. To debug, pass `redirect: "manual"` to your `containerFetch` call and log the `Location` header. To fix, use relative paths in redirects, or set your auth library's base URL to the container's internal HTTP address (for example, `NEXTAUTH_URL=http://localhost:3001` for NextAuth).
- **Debugging container HTTPS errors**: The container binding follows HTTP redirects by default. If your application redirects to an `https://` URL (for example, auth middleware redirecting to a sign-in page), the binding follows the redirect internally and fails. To debug, pass `redirect: "manual"` to your `containerFetch` call and log the response status and `Location` header to see what URL your application is producing.
- **[Error 1016](/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1016/)**: Verify your wildcard DNS record exists and is proxied (orange cloud). If your Worker runs on a subdomain like `sandbox.yourdomain.com`, the DNS record name must be `*.sandbox`, not `*`.
- **[Error 522](/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-522/)**: Verify `proxyToSandbox()` is called first in your Worker's fetch handler -- if it is not handling the request, Cloudflare tries to reach the `192.0.2.0` origin, which times out.

For detailed troubleshooting, see the [Workers routing documentation](/workers/configuration/routing/).

Expand Down