pyngrok - The complete ngrok agent for Python: programmatic tunnels for ingress, inspection, webhooks, demos, and APIs

https://img.shields.io/pypi/v/pyngrok https://img.shields.io/pypi/dm/pyngrok https://img.shields.io/pypi/pyversions/pyngrok.svg https://img.shields.io/codecov/c/github/alexdlaird/pyngrok https://img.shields.io/github/actions/workflow/status/alexdlaird/pyngrok/build.yml https://app.codacy.com/project/badge/Grade/b055cf6e3f1745098fab86a2923730b3 https://img.shields.io/readthedocs/pyngrok https://img.shields.io/github/license/alexdlaird/pyngrok

pyngrok gives Python the complete ngrok agent, with programmatic tunnels for ingress, inspection, webhooks, demos, and APIs. ngrok is a reverse proxy that opens secure tunnels from public URLs to localhost, and because pyngrok runs the real agent, you get everything it offers, from traffic policies and OAuth to its web inspector and request replay, all callable from Python.

Installation

pyngrok is available on PyPI and can be installed using pip:

pip install pyngrok

or conda:

conda install -c conda-forge pyngrok

That’s it! pyngrok is now available as a package to your Python projects, and ngrok is now available from the command line.

Open a Tunnel

To open a tunnel, use the connect method, which returns a NgrokTunnel, and this returned object has a reference to the public URL generated by ngrok in its public_url attribute.

from pyngrok import ngrok

# Open a HTTP tunnel on the default port 80
# <NgrokTunnel: "https://<public_sub>.ngrok.io" -> "http://localhost:80">
http_tunnel = ngrok.connect()

# Open a SSH tunnel
# <NgrokTunnel: "tcp://0.tcp.ngrok.io:12345" -> "localhost:22">
ssh_tunnel = ngrok.connect("22", "tcp")

# Open a named tunnel from the config file
named_tunnel = ngrok.connect(name="my-config-file-tunnel")

# Open an Internal Endpoint that's load balanced
# <NgrokTunnel: "https://some-endpoint.internal" -> "http://localhost:9000">
internal_endpoint = ngrok.connect(addr="9000",
                                  url="https://some-endpoint.internal",
                                  pooling_enabled=True)

The connect method takes kwargs as well, which allows you to pass additional tunnel configurations that are supported by ngrok (or the name of a tunnel defined in ngrok’s config file), as documented here.

Note

pyngrok unifies ngrok’s “tunnel” (v2) and “endpoint” (v3) concepts behind a single API: connect returns an NgrokTunnel and handles the differences for you through the config_version you set. All ngrok features are available to you through this. For v3-specific Endpoints, see Using v3 Endpoints.

Get Active Tunnels

It can be useful to ask the ngrok client what tunnels are currently open. This can be accomplished with the get_tunnels method, which returns a list of NgrokTunnel objects.

from pyngrok import ngrok

# [<NgrokTunnel: "https://<public_sub>.ngrok.io" -> "http://localhost:80">]
tunnels = ngrok.get_tunnels()

Close a Tunnel

All open tunnels will automatically be closed when the Python process terminates, but you can also close them manually with disconnect.

from pyngrok import ngrok

# The NgrokTunnel returned from methods like connect(),
# get_tunnels(), etc. contains the public URL
ngrok.disconnect(ngrok_tunnel.public_url)

Expose Other Services

Using ngrok you can expose any number of non-HTTP services, for instance, databases, game servers, etc. This can be accomplished by using pyngrok to open a tcp tunnel to the desired service.

from pyngrok import ngrok

# Open a tunnel to MySQL with a Reserved TCP Address
# <NgrokTunnel: "tcp://1.tcp.ngrok.io:12345" -> "localhost:3306">
ngrok.connect("3306",
              "tcp",
              url="tcp://1.tcp.ngrok.io:12345")

You can also serve up local directories via ngrok’s built-in fileserver.

from pyngrok import ngrok

# Open a tunnel to a local file server
# <NgrokTunnel: "https://<public_sub>.ngrok.io" -> "file:///">
ngrok.connect("file:///")

Tunnel Configurations

It is possible to configure the tunnel when it is created, for instance adding authentication, a domain, or other additional endpoint configurations that are supported by ngrok. This is accomplished by passing them as kwargs to connect, then they will be used as properties for the tunnel when it is created.

Here is an example that opens a tunnel with the domain some-domain.ngrok.dev, requires basic authentication for requests, and defines a circuit breaker.

from pyngrok import conf, ngrok

# <NgrokTunnel: "https://some-domain.ngrok.dev" -> "http://localhost:80">
ngrok_tunnel = ngrok.connect(url="https://some-domain.ngrok.dev",
                             traffic_policy={
                                 "on_http_request": [{
                                     "actions": [
                                         {"type": "basic-auth", "config": {"credentials": ["username:password"]}},
                                         {"type": "circuit-breaker", "config": {"error_threshold": 0.5}}
                                     ]
                                 }]
                             })

If you already have a tunnel defined in ngrok’s config file, you can start it by its name (note that “-api” will be appended to its name when started).

from pyngrok import conf, ngrok

# <NgrokTunnel: "https://<public_sub>.ngrok.io" -> "http://localhost:??">
ngrok_tunnel = ngrok.connect(name="my-config-file-tunnel")

Using v3 Endpoints

pyngrok defaults to ngrok’s v3 config (its endpoints block is read alongside the tunnels block). v2 addr / proto arguments are translated into the equivalent upstream block automatically. Set config_version to "2" to use the v2 config instead.

version: "3"

endpoints:
  - name: my-config-file-tunnel
    upstream:
      url: http://localhost:8000
      protocol: http1
    pooling_enabled: true
  - name: pyngrok-default
    upstream:
      url: http://localhost:80

You can also open a v3 endpoint without defining it in a config file:

from pyngrok import ngrok

# Open a v3 endpoint
# <NgrokTunnel: "https://<public_sub>.ngrok.io" -> "http://localhost:8000">
endpoint = ngrok.connect(upstream={"url": "http://localhost:8000"},
                         bindings=["public"])

ngrok’s API

The api method allows you to use the local ngrok agent to make requests against the ngrok API, if you have set an API key. For example, here’s how you would reserve a ngrok domain, then create a Cloud Endpoint with an associated traffic policy:

from pyngrok import ngrok

domain = "some-domain.ngrok.dev"
ngrok.api("reserved-domains", "create",
          "--domain", domain)
ngrok.api("endpoints", "create",
          "--bindings", "public",
          "--url", f"https://{domain}",
          "--traffic-policy-file", "policy.yml")

Note

api("endpoints", ...) here invokes ngrok’s agent CLI to manage Cloud Endpoints, which are dashboard-managed and persist independently of any local agent. This is distinct from the local agent Endpoints managed by connect (see Using v3 Endpoints).

The ngrok Process

Opening a tunnel will start the ngrok process. This process will remain alive, and the tunnels open, until kill is invoked, or until the Python process terminates.

If you are building a short-lived app, for instance a CLI, you may want to block on the ngrok process so tunnels stay open until the user intervenes. You can do that by accessing the NgrokProcess.

from pyngrok import ngrok

ngrok_process = ngrok.get_ngrok_process()

try:
    # Block until CTRL-C or some other terminating event
    ngrok_process.proc.wait()
except KeyboardInterrupt:
    print(" Shutting down server.")

    ngrok.kill()

The NgrokProcess contains an api_url variable, usually initialized to http://127.0.0.1:4040, from which you can access the ngrok client API.

Note

If some feature you need is not available in this package, the client API is accessible to you via the api_request method. Additionally, the NgrokTunnel objects expose a uri variable, which contains the relative path used to manipulate that resource against the client API.

This package also gives you access to ngrok from the command line, as documented here.

Event Logs

When ngrok emits logs, pyngrok can surface them to a callback function. To register this callback, use PyngrokConfig and pass the function as log_event_callback. Each time a log is processed, this function will be called, passing a NgrokLog as its only parameter.

from pyngrok import conf, ngrok

def log_event_callback(log):
    print(str(log))

conf.get_default().log_event_callback = log_event_callback

# <NgrokTunnel: "https://<public_sub>.ngrok.io" -> "http://localhost:80">
ngrok_tunnel = ngrok.connect()

If these events aren’t necessary for your use case, some resources can be freed up by turning them off. Set monitor_thread to False in PyngrokConfig.

from pyngrok import conf, ngrok

conf.get_default().monitor_thread = False

# <NgrokTunnel: "https://<public_sub>.ngrok.io" -> "http://localhost:80">
ngrok_tunnel = ngrok.connect()

Alternatively, stop_monitor_thread can be used to stop monitoring on a running process.

import time

from pyngrok import ngrok

# <NgrokTunnel: "https://<public_sub>.ngrok.io" -> "http://localhost:80">
ngrok_tunnel = ngrok.connect()
time.sleep(1)
ngrok.get_ngrok_process().stop_monitor_thread()

Configuration

PyngrokConfig

pyngrok’s interactions with the ngrok binary can be configured using PyngrokConfig. The default pyngrok_config object can be updated with your own object using set_default.

from pyngrok import conf

pyngrok_config = conf.PyngrokConfig(log_event_callback=log_event_callback,
                                    max_logs=10)
conf.set_default(pyngrok_config)

Most methods in the ngrok module also accept a pyngrok_config keyword arg, which can be used to pass in the config rather than updating the default as shown above.

The pyngrok_config argument is only used when the ngrok process is first started, which will be the first time most methods in the ngrok module are called. You can check if a process is already or still running by calling its healthy method.

Note

If ngrok is not already installed at the ngrok_path in PyngrokConfig, it will be installed the first time most methods in the ngrok module are called.

If you need to customize the installation of ngrok, perhaps specifying a timeout, proxy, use a custom mirror for the download, etc., you can do so by leveraging the installer module. Keyword arguments in this module are ultimately passed down to urllib.request.urlopen, so as long as you use the installer module yourself prior to invoking any ngrok methods, you can control how ngrok is installed and from where.

Setting the authtoken or api_key

Running ngrok with an auth token and API key enables access to more features available on your account (for instance, multiple concurrent tunnels, custom domains, use of Internal Endpoints, etc.). You can obtain your auth token and generate API keys from the ngrok dashboard, then install into ngrok’s config file.

from pyngrok import ngrok

# Setting an auth token allows you to open multiple
# tunnels at the same time
ngrok.set_auth_token("<NGROK_AUTHTOKEN>")
# Setting an API key allows you to use things like Internal Endpoints
ngrok.set_api_key("<NGROK_API_KEY>")

# <NgrokTunnel: "https://<public_sub1>.ngrok.io" -> "http://localhost:80">
ngrok_tunnel1 = ngrok.connect()
# <NgrokTunnel: "https://<public_sub2>.ngrok.io" -> "http://localhost:8000">
ngrok_tunnel2 = ngrok.connect("8000")
# <NgrokTunnel: "tls://some-endpoint.internal" -> "localhost:9000">
internal_endpoint = ngrok.connect(addr="9000",
                                  proto="tls",
                                  url="tls://some-endpoint.internal",
                                  pooling_enabled=True)

You can also override ngrok’s installed auth token or API key using PyngrokConfig.

from pyngrok import conf, ngrok

conf.get_default().auth_token = "<NGROK_AUTHTOKEN>"
conf.get_default().api_key = "<NGROK_API_KEY>"

# <NgrokTunnel: "https://<public_sub>.ngrok.io" -> "http://localhost:80">
ngrok_tunnel = ngrok.connect()

Lastly, you could instead define NGROK_AUTHTOKEN or NGROK_API_KEY as environment variables, if you don’t want to define them in code.

Setting the region

By default, ngrok chooses the region with the lowest latency for you. To pick a region instead, set connect_url in ngrok’s config file to that region’s address. The region is set when the ngrok agent starts, so it applies to every tunnel that agent opens.

from pyngrok import conf, installer, ngrok

config_path = "/opt/ngrok/au.yml"
installer.install_default_config(config_path,
                                 {"agent": {"connect_url": "connect.au.ngrok-agent.com:443"}})
conf.get_default().config_path = config_path

# <NgrokTunnel: "https://<public_sub>.au.ngrok.io" -> "http://localhost:80">
ngrok_tunnel = ngrok.connect()

To use more than one region at the same time, start a separate agent for each by giving each PyngrokConfig its own ngrok_path and config_path (pyngrok.aio can start them concurrently).

Config File

By default, ngrok will look for its config file in the default location. You can override this behavior by updating your default PyngrokConfig.

from pyngrok import conf, ngrok

conf.get_default().config_path = "/opt/ngrok/config.yml"

# <NgrokTunnel: "https://<public_sub>.ngrok.io" -> "http://localhost:80">
ngrok_tunnel = ngrok.connect()

Binary Path

The pyngrok package manages its own ngrok binary. You can use your ngrok binary if you want by updating the default PyngrokConfig.

from pyngrok import conf, ngrok

conf.get_default().ngrok_path = "/usr/local/bin/ngrok"

# <NgrokTunnel: "https://<public_sub>.ngrok.io" -> "http://localhost:80">
ngrok_tunnel = ngrok.connect()

Command Line Usage

This package puts the default ngrok binary on your path, so all features of ngrok are also available on the command line.

ngrok http 80

For details on how to fully leverage ngrok from the command line, see ngrok’s official documentation.

Dive Deeper

For more advanced usage, integration examples, securely exposing a local MCP server to remote AI clients, and tips to troubleshoot common issues, dive deeper into the rest of the documentation.

Contributing

If you find issues, report them on GitHub.

If you would like to contribute to the code, the process is pretty simple:

  1. Familiarise yourself with this package, pyngrok’s APIs and other documentation, and ngrok’s documentation.

  2. Fork the repository on GitHub and start implementing changes.

  3. Write a test that plainly validates the changes made.

  4. Build and test locally with make local and make test.

  5. Ensure no linting errors were introduced by running make check.

  6. Submit a pull request to get the changes merged.

Also be sure to review the Code of Conduct before submitting issues or pull requests.

Want to contribute financially? If you’ve found pyngrok useful, sponsorship would also be greatly appreciated!