Skip to main content
A box that runs a web app, a dev server, or an SSH daemon is only useful once something can reach it. On BoxLite Cloud you ask a box for a tunnel to one of its ports, and that tunnel gives you a public URL and a byte stream. What the box itself is allowed to reach on the way out is a separate control, covered at the end of this page.

Prerequisites

  • An API key from the console, exported as BOXLITE_API_KEY. See API keys.
  • pip install boxlite, and the REST URL exported as BOXLITE_REST_URL. See Quickstart.
  • A box you can start, and a service inside it that listens on a TCP port.

Inbound: reach a service inside the box

How a tunnel reaches your service

You do not open a port on the platform. You ask one specific box for a tunnel to one specific port inside it, and await box.network.tunnel(port) prepares it. Behind that single call: the SDK asks the Cloud API to prepare the tunnel (POST /v1/boxes/{id}/network/tunnel if you are driving the REST API yourself), opens a TLS connection to the public proxy, and issues an HTTP CONNECT. The proxy connects on to the runner hosting your box, and the runner connects to the guest port. From there the path is your service’s socket: the tunnel moves bytes and nothing along it interprets your protocol. Two consequences shape everything below.
  • Your service must bind 0.0.0.0. The tunnel terminates on the box’s network interface, so a server bound to 127.0.0.1 inside the box is unreachable — the same reason port forwarding needs 0.0.0.0 on a box you run yourself, explained in Network access.
  • A prepared tunnel carries exactly one connection. See A tunnel is one-shot.

Serve HTTP from a box and get its URL

This script creates a box, starts an HTTP server inside it, prints the tunnel’s public URL, makes a request over the tunnel, and removes the box in finally.
Expected output:
uri() returns the public URL of a tunnel served remotely, which is what a Cloud tunnel is. On a local runtime the same call returns None, because a local tunnel is already a live connection with no address to publish. Reading uri() leaves the tunnel usable — only connect() and forward() spend it. The snippet starts the server with python3. If the server log says python3: not found, install an interpreter into the box first, or start a service the image already ships.

A tunnel is one-shot

connect() and forward() each consume the tunnel. Calling either one a second time on the same BoxTunnel raises a BoxLite error: tunnel connection has already been consumed.Every connection needs a fresh await box.network.tunnel(port). Ask for one per request, per retry, and per concurrent client — as the loop above does — and never cache a BoxTunnel for reuse. Caching the port number is fine; caching the tunnel is the bug.
Preparing a tunnel is cheap and boxes accept several at once, so this is a shape to lean into rather than work around: many concurrent clients on the same guest port each get their own tunnel, and different guest ports on one box can be tunnelled at the same time.

Forward a box port to a local port

forward() publishes the tunnel on an address on your machine, so any TCP client — curl, a browser, a database driver, an existing library that only knows how to dial a socket — can reach the service without knowing about BoxLite.
Three constraints on the listening address:
  • The host must be a numeric IP. SocketAddress.tcp(host="localhost") raises ValueError: tunnel listener host must be a numeric IP — pass "127.0.0.1".
  • port=0 is the default and asks the operating system for a free port. Read the port you actually got from forwarder.local_addr().port.
  • A Unix socket path must be absolute. SocketAddress.unix("relative.sock") raises ValueError: tunnel Unix socket path must be absolute.
await forwarder.wait() blocks until the forwarder finishes, which is what you await in a long-running process instead of exiting. await forwarder.close() shuts it down. The forwarder is built from a tunnel, and forward() spends that tunnel, so build each forwarder from its own fresh await box.network.tunnel(port).

Read and write raw bytes

connect() hands you the byte stream directly. This is the level you work at for a protocol that is not HTTP — a line protocol, a binary framing, a database wire protocol — or when you want full control over what goes on the wire.
read(max_bytes) returns up to max_bytes bytes and an empty bytes object once the far side has closed. max_bytes of 0 raises ValueError: max_bytes must be non-zero, and reading after close() raises a BoxLite error carrying connection is closed. A synchronous surface exists as well: SyncBox.network and SyncBox.tunnel(port) return the SyncNetworkHandle and SyncTunnelForwarder equivalents of the classes documented here.

Parameters and returns

box.network.tunnel(port)

Async. Prepares one tunnel to one port inside one box. Returns a BoxTunnel. await box.tunnel(port) is an equivalent shorthand on a SimpleBox.

BoxTunnel

SocketAddress

Two class methods build the address forward() listens on. Read-only attributes:

TunnelForwarder

BoxConnection

What a tunnel carries

These behaviours are verified end to end against BoxLite Cloud, so you can build on them:
  • HTTP requests and responses, GET and POST.
  • WebSocket — the upgrade handshake and frames in both directions.
  • Several guest ports on one box, tunnelled at the same time.
  • Concurrent clients against the same guest port, each on its own tunnel.
  • Responses larger than 2 MiB through a single connection, byte-for-byte intact.
  • Slow readers — a client that drains the stream slowly does not lose data.
  • Client cancellation — abandoning your side ends that stream without disturbing the box or its other streams.
  • Service restart — restart the server inside the box, open a fresh tunnel, and traffic flows again.
  • Arbitrary TCP, including a real SSH session to an sshd listening on port 2222. The path is not HTTP-specific.
One isolation property is worth stating on its own: two boxes serving on the same guest port never receive each other’s traffic. A tunnel is bound to the box that produced it, and that holds while both boxes take traffic concurrently.

Limits

Three properties of this path to design around. A CONNECT to a stopped box can be accepted before the stream fails. A successful connect is not proof that the box is running — the accept can land and the stream fail immediately afterwards. Treat your first successful read or write as the readiness signal. Tunnel traffic also does not wake a stopped box: the console is explicit that preview traffic keeps a running box alive but does not wake a stopped one, so start the box yourself and check Stop when idle if it stops under you. A response can be lost after shutdown_write(). TCP half-close is not carried end to end, so a guest that waits for end-of-input before it replies may never reply, and a reply already in flight can be dropped. Use a protocol that frames its own messages — a newline, a length prefix, Content-Length — instead of one that signals “done” with EOF. Direct browser use of a tunnel URL is not covered. The URL from uri() is the address the SDK dials when you call connect() or forward(). Navigating to it in a browser, and browser authentication for a private box, sit outside the verified path. Drive tunnels from the SDK, and when you want a browser on the service, forward the port to your machine as shown above.

Outbound: what the box can reach

Inbound and outbound are separate controls. The outbound side of a box’s network boundary is expressed with NetworkSpec, passed as the network field of BoxOptions: mode decides whether the box has a network interface at all, and allow_net narrows egress to a list of hosts. One detail about that allowlist saves a debugging session. A blocked host is a DNS sinkhole, not a connection error: a host that is not on the list resolves to 0.0.0.0. Your code sees a connection to 0.0.0.0 fail, and a nslookup of the blocked host can still exit 0. So when you check whether a host was blocked, inspect the resolved address rather than the exit code. The NetworkSpec parameter table, the three useful mode and allow_net combinations, and the verification recipe live on Network access. That page owns them, and this page does not restate them.

Troubleshooting

Next steps

Boxes on Cloud

Images, sizes, and the lifecycle controls that decide whether your service is still listening.

Quickstart

The five-call Cloud lifecycle, from API key to teardown.

Network access

The NetworkSpec and ports parameter tables, and the egress allowlist in full.