INTEGRITY Cloudflare Docs

Expose services

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:

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

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:

Use cases:

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

Multiple services

Expose multiple ports for full-stack applications:

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

Manage exposed ports

List currently exposed ports

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

Unexpose ports

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

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

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

Best practices

Local development

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

Dockerfile
FROM docker.io/cloudflare/sandbox:0.3.3

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

Update wrangler.jsonc to use your Dockerfile:

wrangler.jsonc
{
  "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 });
// 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 });

Port not ready

Wait for the service to start before exposing:

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

Port already exposed

Check before exposing to avoid errors:

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

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:

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

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

See Sandbox options - normalizeId for details.

Preview URL Format

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

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

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