Port proxy lets external HTTP clients reach any port inside a running sandbox. Each sandbox gets a stable URL pattern based on its ID and port number, so you can start a web server, API, or MCP server inside the VM and access it from outside without SSH tunnels or port-forwarding configuration.
Quick start
Start an HTTP server inside a sandbox, then access it from anywhere using the proxy URL.
from declaw import Sandbox
sbx = Sandbox.create(template="python")
# Start a web server inside the sandbox
sbx.commands.run("nohup python3 -c \"\n"
"from http.server import HTTPServer, BaseHTTPRequestHandler\n"
"class H(BaseHTTPRequestHandler):\n"
" def do_GET(self):\n"
" self.send_response(200)\n"
" self.send_header('Content-Type', 'text/plain')\n"
" self.end_headers()\n"
" self.wfile.write(b'Hello from sandbox!')\n"
"HTTPServer(('', 8080), H).serve_forever()\n"
"\" &>/dev/null &")
import time; time.sleep(1)
# Get the public URL
url = sbx.get_host(8080)
print(url)
# https://api.declaw.ai/sandboxes/sbx-.../ports/8080
import { Sandbox } from '@declaw/sdk';
const sbx = await Sandbox.create({ template: 'node' });
await sbx.commands.run(`nohup node -e "
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello from sandbox!');
}).listen(8080);
" &>/dev/null &`);
const url = sbx.getHost(8080);
console.log(url);
// https://api.declaw.ai/sandboxes/sbx-.../ports/8080
sbx, _ := declaw.Create(ctx, declaw.WithTemplate("base"))
sbx.Commands.Run(ctx, `nohup python3 -c "
from http.server import HTTPServer, BaseHTTPRequestHandler
class H(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(b'Hello from sandbox!')
HTTPServer(('', 8080), H).serve_forever()
" &>/dev/null &`)
url := sbx.GetHost(8080)
fmt.Println(url)
// https://api.declaw.ai/sandboxes/sbx-.../ports/8080
The returned URL is a fully qualified HTTPS endpoint. Any HTTP client (browser, curl, SDK) can send requests to it, authenticated with your API key in the X-API-Key header.
How it works
- Your client sends an HTTP request to
https://api.declaw.ai/sandboxes/{sandbox_id}/ports/{port}/{path}.
- Declaw authenticates the request using the
X-API-Key header.
- If
allow_public_traffic is enabled (the default), the request is proxied to the target port inside the sandbox.
- The response from the in-sandbox server is returned to the client.
For WebSocket requests, the same auth and validation runs first. Once authenticated, the connection is upgraded and Declaw maintains a bidirectional tunnel between your client and the in-sandbox server for the lifetime of the connection.
Subpaths are preserved. A request to .../ports/8080/api/v1/users reaches the sandbox as GET /api/v1/users.
MCP servers
Declaw provides a convenience method for the common pattern of running an MCP server on port 50005 inside a sandbox.
url = sbx.get_mcp_url()
print(url)
# https://api.declaw.ai/sandboxes/sbx-.../ports/50005/mcp
const url = sbx.getMcpUrl();
console.log(url);
// https://api.declaw.ai/sandboxes/sbx-.../ports/50005/mcp
url := sbx.GetMcpURL()
fmt.Println(url)
// https://api.declaw.ai/sandboxes/sbx-.../ports/50005/mcp
This is equivalent to calling get_host(50005) + "/mcp". Use the mcp-server template to get a sandbox pre-configured with FastMCP and the standard MCP dependencies.
WebSocket
Port proxy supports WebSocket connections. Use the same URL from get_host(), replacing https:// with wss://.
import asyncio
import websockets
from declaw import Sandbox
sbx = Sandbox.create(template="python")
# Start a WebSocket echo server inside the sandbox
sbx.commands.run("pip install websockets -q", timeout=30)
sbx.files.write("/tmp/ws.py", """
import asyncio, websockets
async def echo(ws):
async for msg in ws:
await ws.send("echo: " + msg)
async def main():
async with websockets.serve(echo, "0.0.0.0", 8765):
await asyncio.Future()
asyncio.run(main())
""")
sbx.commands.run("nohup python3 /tmp/ws.py &>/dev/null &")
import time; time.sleep(2)
# Connect via WebSocket
url = sbx.get_host(8765).replace("https://", "wss://")
async def main():
async with websockets.connect(url, additional_headers={"X-API-Key": "YOUR_API_KEY"}) as ws:
await ws.send("hello")
print(await ws.recv()) # "echo: hello"
asyncio.run(main())
import { Sandbox } from '@declaw/sdk';
const sbx = await Sandbox.create({ template: 'node' });
// Start a WebSocket server inside the sandbox
await sbx.commands.run(`npm install ws -q && nohup node -e "
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 8765 });
wss.on('connection', ws => {
ws.on('message', msg => ws.send('echo: ' + msg));
});
" &>/dev/null &`);
// Connect via WebSocket (requires the 'ws' package)
import WebSocket from 'ws';
const url = sbx.getHost(8765).replace('https://', 'wss://');
const ws = new WebSocket(url, { headers: { 'X-API-Key': 'YOUR_API_KEY' } });
sbx, _ := declaw.Create(ctx, declaw.WithTemplate("python"))
url := strings.Replace(sbx.GetHost(8765), "https://", "wss://", 1)
// Connect using gorilla/websocket or any WS client library
WebSocket connections require the same X-API-Key header as HTTP requests, passed during the upgrade handshake via additional_headers (Python) or connection options (TypeScript/Go).
Security
Authentication
All port proxy requests require a valid X-API-Key header, the same key used for every other sandbox operation. Unauthenticated requests receive HTTP 401.
Disabling port proxy
Port proxy access is controlled by the allow_public_traffic field in the sandbox’s network configuration. It defaults to true. Set it to false to block all inbound port proxy requests.
from declaw import Sandbox
sbx = Sandbox.create(
network={"allow_public_traffic": False}
)
# Requests to sbx.get_host(8080) will receive HTTP 403
import { Sandbox } from '@declaw/sdk';
const sbx = await Sandbox.create({
network: { allowOut: [], denyOut: [], allowPublicTraffic: false },
});
f := false
sbx, _ := declaw.Create(ctx, declaw.WithNetwork(declaw.SandboxNetworkOpts{
AllowPublicTraffic: &f,
}))
Blocked ports
Port 49983 is reserved for internal use and cannot be proxied. Requests targeting this port return HTTP 403.
Cookie stripping
Set-Cookie headers are stripped from all proxied responses to prevent cookie injection attacks against the API domain.
Limits
| Limit | Value |
|---|
| Request body size | 100 MiB (HTTP only) |
| Supported HTTP methods | GET, POST, PUT, PATCH, DELETE, HEAD |
| WebSocket | Supported (upgrade via GET) |
| Blocked ports | 49983 (reserved) |
CORS
CORS preflight (OPTIONS) requests are handled automatically by the proxy and do not require an X-API-Key header. This allows browser-based applications to call sandbox services directly without a backend relay.
All other HTTP methods (GET, POST, etc.) still require the X-API-Key header, even when called from a browser. Use a backend proxy or serverless function if you cannot expose your API key to the client.