Skip to main content
Sailboxes are closed to inbound traffic by default. To publish a service, pass the guest port in ingress_ports when you create the Sailbox, start a process that listens on that port, and wait for the listener endpoint to become ready. You can also add and remove ports on a running Sailbox with expose/unexpose (see Add or remove ports at runtime). Sail supports two inbound protocols:
  • HTTP, which returns a public HTTPS URL and supports HTTP and WebSocket traffic.
  • Raw TCP, which returns a public host and port for protocols such as SSH, Postgres, or custom TCP servers.
Add or remove ports at any time (see below). Terminating the Sailbox removes all of its listeners.

HTTP and WebSocket services

Pass a bare port number in ingress_ports to expose it over HTTP. The listener resolves to an HttpEndpoint with a routable HTTPS url.
import sail

app = sail.App.find(name="web-demo", mint_if_missing=True)

sb = sail.Sailbox.create(
    app=app,
    image=sail.Image.debian_arm64,
    name="web-demo",
    ingress_ports=[3000],
)

sb.exec("mkdir -p /srv/app && echo hello > /srv/app/index.html").wait()
sb.exec("python3 -m http.server 3000", background=True, cwd="/srv/app").wait()

listener = sb.listener(3000)
listener.wait(timeout=60)

print(listener.endpoint.url)
Use the same URL for WebSocket clients by replacing https:// with wss:// when the process inside the Sailbox speaks WebSocket on that port.
ws_url = listener.endpoint.url.replace("https://", "wss://")

Raw TCP ports

Pass sail.IngressPort(port, "tcp") to expose a port as raw TCP instead of HTTP. Use raw TCP when the protocol is not HTTP-aware or the service already implements its own authentication. The listener resolves to a TcpEndpoint with a host and port that any TCP client can dial directly.
sb = sail.Sailbox.create(
    app=app,
    image=sail.Image.debian_arm64,
    name="tcp-demo",
    ingress_ports=[sail.IngressPort(5432, "tcp", cidr_allowlist=["203.0.113.0/24"])],
)
After the service starts, wait for the endpoint and connect with the matching client:
listener = sb.listener(5432).wait(timeout=60)
host, port = listener.endpoint.host, listener.endpoint.port
print(f"postgres://user:password@{host}:{port}/postgres")

SSH access

Every Sailbox image ships with an SSH server installed. Enable SSH access by specifying ssh=True at create time, or by calling sb.enable_ssh() on any running Sailbox. Both expose guest port 22, add your public key, and start the SSH daemon.
import sail

sb = sail.Sailbox.create(
    app=app,
    image=sail.Image.debian_arm64,
    name="ssh-demo",
    ssh=True,
)

endpoint = sb.listener(22).wait(timeout=60).endpoint
print(f"ssh -p {endpoint.port} root@{endpoint.host}")
ssh=True installs your local ~/.ssh/id_ed25519.pub key, falling back to ~/.ssh/id_rsa.pub if needed. Pass ssh="<path-or-key>" to choose a different public key. The listener endpoint gives the host and port to connect with your own ssh client, scp, SSH config, or IDE. From the CLI, sail box create --enable-ssh runs the same setup and prints the SSH command. The SSH server persists across sleep and resume. An open SSH session drops when the Sailbox sleeps, but reconnecting with plain ssh wakes it and uses the same host key. If sshd stops inside a running Sailbox, call sb.enable_ssh() again; this operation is idempotent.

Inspect endpoints

Use listener(port) when you know the guest port, or listeners() to list all published ports for a Sailbox:
for listener in sb.listeners():
    listener = listener.wait(timeout=60)
    if listener.protocol == "http":
        print(listener.port, listener.endpoint.url)
    else:
        endpoint = listener.endpoint
        print(listener.port, f"{endpoint.host}:{endpoint.port}")
HTTP listeners expose listener.endpoint.url. TCP listeners expose listener.endpoint.host and listener.endpoint.port.

Add or remove ports at runtime

You don’t have to declare every port at create time. expose publishes a new port on a running Sailbox and unexpose removes one, with no guest restart.
# Add an HTTP port and read its URL.
http = sb.expose(8080)
print(http.wait(timeout=60).endpoint.url)

# Add a CIDR-restricted raw-TCP port, then remove it when you're done.
sb.expose(5432, protocol="tcp", cidr_allowlist=["203.0.113.0/24"])
sb.unexpose(5432)
Re-exposing a port under the same protocol updates its cidr_allowlist to the value you pass. Use it to tighten or relax a live raw-TCP port. Unexposing a raw-TCP port stops serving it and frees its endpoint-quota slot, but keeps its public host:port reserved to your Sailbox. Re-expose the same guest port to reclaim the exact address, and no other Sailbox ever takes it. A raw-TCP guest port therefore can’t be repurposed to HTTP; use a different guest port. HTTP ports carry no such reservation and are released on unexpose. expose and unexpose work on a paused or sleeping Sailbox without waking it; a later resume serves the new listener. From the CLI:
sail box expose <id> 8080
sail box expose <id> 5432 --tcp --cidr 203.0.113.0/24
sail box unexpose <id> 5432

Access controls

A raw-TCP port is reachable from the public internet with no platform-side authentication. The in-guest daemon, such as sshd, is the only access control, so make sure it requires credentials.
To restrict which source IPs may connect, pass cidr_allowlist:
ingress_ports=[
    sail.IngressPort(22, "tcp", cidr_allowlist=["203.0.113.0/24", "198.51.100.7/32"]),
]
Connections from outside the listed CIDR prefixes are dropped before reaching the guest. An empty or omitted cidr_allowlist means any source may connect. cidr_allowlist is a TCP-only control; it is not accepted on HTTP ports. Exposing a well-known unauthenticated service port, such as Postgres, MySQL, or Redis, as raw TCP with neither a cidr_allowlist nor allow_public=True is rejected. Set a cidr_allowlist, or pass allow_public=True to confirm you want it publicly reachable:
ingress_ports=[
    sail.IngressPort(5432, "tcp", allow_public=True),
]

Sleeping services

Sleeping Sailboxes wake on network ingress. If you call sleep() on a Sailbox with exposed listeners, the next inbound HTTP, WebSocket, or TCP connection wakes the VM before forwarding traffic to the guest process.
sb.sleep()
# A later request to listener.endpoint.url wakes the Sailbox.
Use pause() instead when you want to preserve VM state without waking on network traffic.

Ports and cleanup

Ports must be unique within a Sailbox and between 1 and 65535. Ports 10000, 10001, 15001, and 15002 are reserved by the platform. Port 22 is reserved for HTTP ingress but is available for raw TCP ingress. Each org can hold a limited number (32) of concurrent raw-TCP endpoints. The limit counts actively-exposed endpoints, so unexpose frees a slot. The port’s host:port stays reserved to your Sailbox but no longer counts. Contact us to raise your limit if you need more concurrent endpoints. (A reserved host:port is never reused by another Sailbox, even after unexpose or termination.)