跳转到主要内容

Expose services

import { TypeScriptExample, Render } from "~/components";

<Render file="sdk-0x-note" product="sandbox" params={{ href: "/sandbox/previews/", title: "Preview a web application" }} />

:::note[Production requires custom domain] Preview URLs require a custom domain with wildcard DNS routing in production. See Configure preview URLs on a custom domain for setup instructions. :::

:::note[Prefer sandbox.tunnels for public URLs] sandbox.tunnels is the recommended option for most public-URL use cases, including production. Quick tunnels give you a zero-config *.trycloudflare.com URL; named tunnels bind a stable <name>.<your-zone> hostname. Follow this guide when you specifically want the Worker itself to front the request (for example, to inject authentication or rewrite responses). :::

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

When to expose ports

Expose ports when you need to:

  • Test web applications - Preview frontend or backend apps
  • Share demos - Give others access to running applications
  • Develop APIs - Test endpoints from external tools
  • Debug services - Access internal services for troubleshooting
  • Build dev environments - Create shareable development workspaces

Basic port exposure

The typical workflow is: start service → wait for ready → expose port → handle requests with proxyToSandbox.

``` import { getSandbox, proxyToSandbox } from '@cloudflare/sandbox';

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

export default { async fetch(request: Request, env: Env): Promise { // Proxy requests to exposed ports first const proxyResponse = await proxyToSandbox(request, env); if (proxyResponse) return proxyResponse;

// Extract hostname from request
const { hostname } = new URL(request.url);
const sandbox = getSandbox(env.Sandbox, 'my-sandbox');

// 1. Start a web server
await sandbox.startProcess('python -m http.server 8000');

// 2. Wait for service to start
await new Promise(resolve => setTimeout(resolve, 2000));

// 3. Expose the port
const exposed = await sandbox.exposePort(8000, { hostname });

// 4. Preview URL is now available (public by default)
console.log('Server accessible at:', exposed.url);
// Production: https://8000-abc123.yourdomain.com
// Local dev: http://localhost:8787/...

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

} };

</TypeScriptExample>

:::caution
**Preview URLs are public by default.** Anyone with the URL can access your service. Add authentication if needed.
:::

:::note[Local development requirement]
When using `wrangler dev`, you must add `EXPOSE` directives to your Dockerfile for each port you plan to expose. Without this, you'll see "Connection refused: container port not found". See [Local development](#local-development) section below for setup details.
:::

:::caution[Uppercase sandbox IDs don't work with preview URLs]
Preview URLs extract the sandbox ID from the hostname, which is always lowercase (e.g., `8000-myproject-123.yourdomain.com`). If you created your sandbox with an uppercase ID like `"MyProject-123"`, the URL routes to `"myproject-123"` (a different Durable Object), making your sandbox unreachable.

To fix this, use `normalizeId: true` when creating sandboxes for port exposure:

```ts
const sandbox = getSandbox(env.Sandbox, 'MyProject-123', { normalizeId: true });

This lowercases the ID during creation so it matches preview URL routing. Without this, exposePort() throws an error.

Best practice: Use lowercase IDs from the start ('my-project-123').

See Sandbox options for details. :::

Stable URLs with custom tokens

For production deployments or when sharing URLs with users, use custom tokens to maintain consistent preview URLs across container restarts:

``` // Extract hostname from request const { hostname } = new URL(request.url);

// Without custom token - URL changes on restart const exposed = await sandbox.exposePort(8080, { hostname }); // https://8080-sandbox-id-random16chars12.yourdomain.com

// With custom token - URL stays the same across restarts const stable = await sandbox.exposePort(8080, { hostname, token: 'api-v1' }); // https://8080-sandbox-id-api-v1.yourdomain.com // Same URL after container restart ✓

return Response.json({ 'Temporary URL (changes on restart)': exposed.url, 'Stable URL (consistent)': stable.url });

</TypeScriptExample>

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

**Use cases:**
- Production APIs with stable endpoints
- Sharing demo URLs with external users
- Integration testing with predictable URLs
- Documentation with consistent examples

## Name your exposed ports

When exposing multiple ports, use names to stay organized:

<TypeScriptExample>

// Extract hostname from request const { hostname } = new URL(request.url);

// Start and expose API server with stable token await sandbox.startProcess('node api.js', { env: { PORT: '8080' } }); await new Promise(resolve => setTimeout(resolve, 2000)); const api = await sandbox.exposePort(8080, { hostname, name: 'api', token: 'api-prod' });

// Start and expose frontend with stable token await sandbox.startProcess('npm run dev', { env: { PORT: '5173' } }); await new Promise(resolve => setTimeout(resolve, 2000)); const frontend = await sandbox.exposePort(5173, { hostname, name: 'frontend', token: 'web-app' });

console.log('Services:'); console.log('- API:', api.url); console.log('- Frontend:', frontend.url);

</TypeScriptExample>

## Wait for service readiness

Always verify a service is ready before exposing. Use a simple delay for most cases:

<TypeScriptExample>

// Extract hostname from request const { hostname } = new URL(request.url);

// Start service await sandbox.startProcess('npm run dev', { env: { PORT: '8080' } });

// Wait 2-3 seconds await new Promise(resolve => setTimeout(resolve, 2000));

// Now expose await sandbox.exposePort(8080, { hostname });

</TypeScriptExample>

For critical services, poll the health endpoint:

<TypeScriptExample>

// Extract hostname from request const { hostname } = new URL(request.url);

await sandbox.startProcess('node api-server.js', { env: { PORT: '8080' } });

// Wait for health check for (let i = 0; i < 10; i++) { await new Promise(resolve => setTimeout(resolve, 1000));

const check = await sandbox.exec('curl -f http://localhost:8080/health || echo "not ready"'); if (check.stdout.includes('ok')) { break; } }

await sandbox.exposePort(8080, { hostname });

</TypeScriptExample>

## Multiple services

Expose multiple ports for full-stack applications:

<TypeScriptExample>

// Extract hostname from request const { hostname } = new URL(request.url);

// Start backend await sandbox.startProcess('node api/server.js', { env: { PORT: '8080' } }); await new Promise(resolve => setTimeout(resolve, 2000));

// Start frontend await sandbox.startProcess('npm run dev', { cwd: '/workspace/frontend', env: { PORT: '5173', API_URL: 'http://localhost:8080' } }); await new Promise(resolve => setTimeout(resolve, 3000));

// Expose both const api = await sandbox.exposePort(8080, { hostname, name: 'api' }); const frontend = await sandbox.exposePort(5173, { hostname, name: 'frontend' });

return Response.json({ api: api.url, frontend: frontend.url });

</TypeScriptExample>

## Manage exposed ports

### List currently exposed ports

<TypeScriptExample>

const { ports, count } = await sandbox.getExposedPorts();

console.log(${count} ports currently exposed:);

for (const port of ports) { console.log( Port ${port.port}: ${port.url}); if (port.name) { console.log( Name: ${port.name}); } }

</TypeScriptExample>

### Unexpose ports

<TypeScriptExample>

// Unexpose a single port await sandbox.unexposePort(8000);

// Unexpose multiple ports for (const port of [3000, 5173, 8080]) { await sandbox.unexposePort(port); }

</TypeScriptExample>

## Best practices

- **Wait for readiness** - Don't expose ports immediately after starting processes
- **Use named ports** - Easier to track when exposing multiple ports
- **Clean up** - Unexpose ports when done to prevent abandoned URLs
- **Add authentication** - Preview URLs are public; protect sensitive services

## Local development

When developing locally with `wrangler dev`, you must expose ports in your Dockerfile:

```dockerfile title="Dockerfile"
FROM docker.io/cloudflare/sandbox:0.12.14

# Expose ports you plan to use
EXPOSE 8000
EXPOSE 8080
EXPOSE 5173

Update wrangler.jsonc to use your Dockerfile:

{
  "containers": [
    {
      "class_name": "Sandbox",
      "image": "./Dockerfile"
    }
  ]
}

In production, all ports are available and controlled programmatically via exposePort() / unexposePort().

Troubleshooting

Port 3000 is reserved

Port 3000 is used by the internal Bun server and cannot be exposed:

``` // Extract hostname from request const { hostname } = new URL(request.url);

// ❌ This will fail await sandbox.exposePort(3000, { hostname }); // Error: Port 3000 is reserved

// ✅ Use a different port await sandbox.startProcess('node server.js', { env: { PORT: '8080' } }); await sandbox.exposePort(8080, { hostname });

</TypeScriptExample>

### Port not ready

Wait for the service to start before exposing:

<TypeScriptExample>

// Extract hostname from request const { hostname } = new URL(request.url);

await sandbox.startProcess('npm run dev'); await new Promise(resolve => setTimeout(resolve, 3000)); await sandbox.exposePort(8080, { hostname });

</TypeScriptExample>

### Port already exposed

Check before exposing to avoid errors:

<TypeScriptExample>

// Extract hostname from request const { hostname } = new URL(request.url);

const { ports } = await sandbox.getExposedPorts(); if (!ports.some(p => p.port === 8080)) { await sandbox.exposePort(8080, { hostname }); }

</TypeScriptExample>

### Uppercase sandbox ID error

**Error**: `Preview URLs require lowercase sandbox IDs`

**Cause**: You created a sandbox with uppercase characters (e.g., `"MyProject-123"`) but preview URLs always use lowercase in routing, causing a mismatch.

**Solution**:

<TypeScriptExample>

// Create sandbox with normalization const sandbox = getSandbox(env.Sandbox, 'MyProject-123', { normalizeId: true }); await sandbox.exposePort(8080, { hostname });

</TypeScriptExample>

This creates the Durable Object with ID `"myproject-123"`, matching the preview URL routing.

See [Sandbox options - normalizeId](/sandbox/sdk/configuration/sandbox-options/#normalizeid) for details.

## Preview URL Format

**Production**: `https://{port}-{sandbox-id}-{token}.yourdomain.com`

- Auto-generated token: `https://8080-abc123-random16chars12.yourdomain.com`
- Custom token: `https://8080-abc123-my-api-v1.yourdomain.com`

**Local development**: `http://localhost:8787/...`

**Note**: Port 3000 is reserved for the internal Bun server and cannot be exposed.

## Related resources

- [Ports API reference](/sandbox/sdk/api/ports/) - Complete port exposure API
- [Background processes guide](/sandbox/sdk/guides/background-processes/) - Managing services
- [Execute commands guide](/sandbox/sdk/guides/execute-commands/) - Starting services
- [Tunnels API reference](/sandbox/sdk/api/tunnels/) - Recommended alternative for most public-URL use cases (quick or named tunnels)