Expose services

Last updated Oct 23, 2026 Note

This page documents Sandbox SDK 0.x for existing applications. For new applications, refer to Preview a web application. To move an existing application to @cloudflare/sandbox 1.0, refer to Migrate from Sandbox SDK 0.x.

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.

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, env) {
        // 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 });
    },
};
import { getSandbox, proxyToSandbox } from '@cloudflare/sandbox';

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

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // 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 });
  }
};

Caution

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

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 section below for setup details.

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:

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,
});
// 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
});

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:

// 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);
// 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);

Wait for service readiness

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

// 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 });
// 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 });

For critical services, poll the health endpoint:

// 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 });