Check out our new GPU Glossary! Read now

modal.forward

@contextmanager
def forward(port: int, *, unencrypted: bool = False, client: Optional[_Client] = None) -> Iterator[Tunnel]:

Expose a port publicly from inside a running Modal container, with TLS.

If unencrypted is set, this also exposes the TCP socket without encryption on a random port number. This can be used to SSH into a container (see example below). Note that it is on the public Internet, so make sure you are using a secure protocol over TCP.

Important: This is an experimental API which may change in the future.

Usage:

import modal
from flask import Flask

app = modal.App(image=modal.Image.debian_slim().pip_install("Flask"))
flask_app = Flask(__name__)


@flask_app.route("/")
def hello_world():
    return "Hello, World!"


@app.function()
def run_app():
    # Start a web server inside the container at port 8000. `modal.forward(8000)` lets us
    # expose that port to the world at a random HTTPS URL.
    with modal.forward(8000) as tunnel:
        print("Server listening at", tunnel.url)
        flask_app.run("0.0.0.0", 8000)

    # When the context manager exits, the port is no longer exposed.

Raw TCP usage:

import socket
import threading

import modal


def run_echo_server(port: int):
    """Run a TCP echo server listening on the given port."""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(("0.0.0.0", port))
    sock.listen(1)

    while True:
        conn, addr = sock.accept()
        print("Connection from:", addr)

        # Start a new thread to handle the connection
        def handle(conn):
            with conn:
                while True:
                    data = conn.recv(1024)
                    if not data:
                        break
                    conn.sendall(data)

        threading.Thread(target=handle, args=(conn,)).start()


app = modal.App()


@app.function()
def tcp_tunnel():
    # This exposes port 8000 to public Internet traffic over TCP.
    with modal.forward(8000, unencrypted=True) as tunnel:
        # You can connect to this TCP socket from outside the container, for example, using `nc`:
        #  nc <HOST> <PORT>
        print("TCP tunnel listening at:", tunnel.tcp_socket)
        run_echo_server(8000)

SSH example: This assumes you have a rsa keypair in ~/.ssh/id_rsa{.pub}, this is a bare-bones example letting you SSH into a Modal container.

import subprocess
import time

import modal

app = modal.App()
image = (
    modal.Image.debian_slim()
    .apt_install("openssh-server")
    .run_commands("mkdir /run/sshd")
    .copy_local_file("~/.ssh/id_rsa.pub", "/root/.ssh/authorized_keys")
)


@app.function(image=image, timeout=3600)
def some_function():
    subprocess.Popen(["/usr/sbin/sshd", "-D", "-e"])
    with modal.forward(port=22, unencrypted=True) as tunnel:
        hostname, port = tunnel.tcp_socket
        connection_cmd = f'ssh -p {port} root@{hostname}'
        print(f"ssh into container using: {connection_cmd}")
        time.sleep(3600)  # keep alive for 1 hour or until killed

If you intend to use this more generally, a suggestion is to put the subprocess and port forwarding code in an @enter lifecycle method of an @app.cls, to only make a single ssh server and port for each container (and not one for each input to the function).