← Sandbox / sandbox / guides
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:
- 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 });
}
};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 });// 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
- 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:
FROM docker.io/cloudflare/sandbox:0.3.3
# Expose ports you plan to use
EXPOSE 8000
EXPOSE 8080
EXPOSE 5173Update 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 });// 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
- 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 - Complete port exposure API
- Background processes guide - Managing services
- Execute commands guide - Starting services
- Tunnels API reference - Recommended alternative for most public-URL use cases (quick or named tunnels)