Integration Examples¶
pyngrok
is useful in any number of integrations, for instance to test locally without having to deploy or configure
anything. Below are some common usage examples.
Flask¶
The Flask example includes a Dockerfile
to highlight a containerized use case.
In server.py
, where your Flask app is initialized,
you should add a variable that let’s you configure from an environment variable whether you want to open a tunnel
to localhost
with ngrok
when the dev server starts. You can initialize the pyngrok
tunnel in this
same place.
import os
import sys
from flask import Flask
def init_webhooks(base_url):
# ... Implement updates necessary so inbound traffic uses the public-facing ngrok URL
pass
def create_app():
app = Flask(__name__)
# Initialize your ngrok settings into Flask
app.config.from_mapping(
BASE_URL="http://localhost:5000",
USE_NGROK=os.environ.get("USE_NGROK", "False") == "True" and os.environ.get("WERKZEUG_RUN_MAIN") != "true"
)
if app.config["USE_NGROK"]:
# Only import pyngrok and install if we're actually going to use it
from pyngrok import ngrok
# Get the dev server port (defaults to 5000 for Flask, can be overridden with `--port`
# when starting the server
port = sys.argv[sys.argv.index("--port") + 1] if "--port" in sys.argv else "5000"
# Open a ngrok tunnel to the dev server
public_url = ngrok.connect(port).public_url
print(f" * ngrok tunnel \"{public_url}\" -> \"http://127.0.0.1:{port}\"")
# Update any base URLs or webhooks to use the public ngrok URL
app.config["BASE_URL"] = public_url
init_webhooks(public_url)
# ... Implement Blueprints and the rest of your app
return app
Now Flask can be started in development by the usual means, setting USE_NGROK
to open a tunnel.
USE_NGROK=True NGROK_AUTHTOKEN=$NGROK_AUTHTOKEN \
FLASK_APP=server.py \
flask run
Django¶
In settings.py of
your Django project, you should add a
variable that let’s you configure from an environment variable whether you want to open a tunnel to
localhost
with ngrok
when the dev server starts.
import os
import sys
# ... Implement the rest of your Django settings
BASE_URL = "http://localhost:8000"
USE_NGROK = os.environ.get("USE_NGROK", "False") == "True" and os.environ.get("RUN_MAIN", None) != "true"
If this flag is set, you want to initialize pyngrok
when Django is booting from its dev server. An easy place
to do this is one of your apps.py
by extending AppConfig.
import os
import sys
from urllib.parse import urlparse
from django.apps import AppConfig
from django.conf import settings
class CommonConfig(AppConfig):
name = "myproject.common"
verbose_name = "Common"
def ready(self):
if settings.USE_NGROK:
# Only import pyngrok and install if we're actually going to use it
from pyngrok import ngrok
# Get the dev server port (defaults to 8000 for Django, can be overridden with the
# last arg when calling `runserver`)
addrport = urlparse(f"http://{sys.argv[-1]}")
port = addrport.port if addrport.netloc and addrport.port else "8000"
# Open a ngrok tunnel to the dev server
public_url = ngrok.connect(port).public_url
print(f"ngrok tunnel \"{public_url}\" -> \"http://127.0.0.1:{port}\"")
# Update any base URLs or webhooks to use the public ngrok URL
settings.BASE_URL = public_url
CommonConfig.init_webhooks(public_url)
@staticmethod
def init_webhooks(base_url):
# ... Implement updates necessary so inbound traffic uses the public-facing ngrok URL
pass
Now the Django dev server can be started by the usual means, setting USE_NGROK
to open a tunnel.
USE_NGROK=True NGROK_AUTHTOKEN=$NGROK_AUTHTOKEN \
python manage.py runserver
FastAPI¶
In server.py
, where your FastAPI app is initialized,
you should add a variable that let’s you configure from an environment variable whether you want to tunnel to
localhost
with ngrok
. You can initialize the pyngrok
tunnel in this same place.
import os
import sys
from fastapi import FastAPI
from fastapi.logger import logger
from pydantic import BaseSettings
class Settings(BaseSettings):
# ... Implement the rest of your FastAPI settings
BASE_URL = "http://localhost:8000"
USE_NGROK = os.environ.get("USE_NGROK", "False") == "True"
settings = Settings()
def init_webhooks(base_url):
# ... Implement updates necessary so inbound traffic uses the public-facing ngrok URL
pass
# Initialize the FastAPI app for a simple web server
app = FastAPI()
if settings.USE_NGROK:
# Only import pyngrok and install if we're actually going to use it
from pyngrok import ngrok
# Get the dev server port (defaults to 8000 for Uvicorn, can be overridden with `--port`
# when starting the server
port = sys.argv[sys.argv.index("--port") + 1] if "--port" in sys.argv else "8000"
# Open a ngrok tunnel to the dev server
public_url = ngrok.connect(port).public_url
logger.info(f"ngrok tunnel \"{public_url}\" -> \"http://127.0.0.1:{port}\"")
# Update any base URLs or webhooks to use the public ngrok URL
settings.BASE_URL = public_url
init_webhooks(public_url)
# ... Implement routers and the rest of your app
Now FastAPI can be started by the usual means, with Uvicorn, setting
USE_NGROK
to open a tunnel.
USE_NGROK=True NGROK_AUTHTOKEN=$NGROK_AUTHTOKEN \
uvicorn server:app
Docker¶
pyngrok
provides pre-built container images on Docker Hub, where
you’ll also find usage examples and a breakdown of the image tags available.
To launch the container in to a Python shell, run:
docker run \
-e NGROK_AUTHTOKEN=$NGROK_AUTHTOKEN -it alexdlaird/pyngrok
If you want to start in a bash
shell instead of Python, you can launch the container with:
docker run \
-e NGROK_AUTHTOKEN=$NGROK_AUTHTOKEN -it alexdlaird/pyngrok \
/bin/bash
The pyngrok-example-flask repository also includes a
Dockerfile
and make
commands to run it, if you would like to see a complete example.
Config File¶
ngrok
will look for its config file in this container at /root/.config/ngrok/ngrok.yml
. If you want to provide a
custom config file, specify a mount to this file when launching the container.
docker run \
-v ./ngrok.yml:/root/.config/ngrok/ngrok.yml -it alexdlaird/pyngrok
Web Inspector¶
If you want to use ngrok
’s web inspector, be sure to expose its port. Be sure whatever config file you use
sets web_addr: 0.0.0.0:4040 (the config provisioned in the
pre-built images already does this).
docker run --env-file .env -p 4040:4040 -it alexdlaird/pyngrok
Docker Compose¶
Here is an example of how you could launch the container using docker-compose.yml
, where you also want a given
Python script to run on startup:
services:
ngrok:
image: alexdlaird/pyngrok
env_file: ".env"
command:
- "python /root/my-script.py"
volumes:
- ./my-script.py:/root/my-script.py
ports:
- 4040:4040
Then launch it with:
docker compose up -d
Command Line Usage¶
pyngrok
package puts the default ngrok
binary on your path in the container, so all features of ngrok
are
also available on the command line.
docker run \
-e NGROK_AUTHTOKEN=$NGROK_AUTHTOKEN -it alexdlaird/pyngrok \
ngrok http 80
For details on how to fully leverage ngrok
from the command line, see ngrok’s official documentation.
Google Colaboratory¶
Using ngrok
in a Google Colab Notebook
takes just two code cells with pyngrok
. Install pyngrok
as a dependency in your Notebook by creating a code
block like this:
!pip install pyngrok
Colab SSH Example¶
With an SSH server setup and running (as shown fully in the linked example), all you need to do is create another code
cell that uses pyngrok
to open a tunnel to that server.
import getpass
from pyngrok import ngrok, conf
print("Enter your authtoken, which can be copied from https://dashboard.ngrok.com/get-started/your-authtoken")
conf.get_default().auth_token = getpass.getpass()
# Open a TCP ngrok tunnel to the SSH server
connection_string = ngrok.connect("22", "tcp").public_url
ssh_url, port = connection_string.strip("tcp://").split(":")
print(f" * ngrok tunnel available, access with `ssh root@{ssh_url} -p{port}`")
Colab HTTP Example¶
It can also be useful to expose a web server, process HTTP requests, etc. from within your Notebook. This code block
assumes you have also added !pip install flask
to your dependency code block.
import os
import threading
from flask import Flask
from pyngrok import ngrok
app = Flask(__name__)
port = "5000"
# Open a ngrok tunnel to the HTTP server
public_url = ngrok.connect(port).public_url
print(f" * ngrok tunnel \"{public_url}\" -> \"http://127.0.0.1:{port}\"")
# Update any base URLs to use the public ngrok URL
app.config["BASE_URL"] = public_url
# ... Implement updates necessary so inbound traffic uses the public-facing ngrok URL
# Define Flask routes
@app.route("/")
def index():
return "Hello from Colab!"
# Start the Flask server in a new thread
threading.Thread(target=app.run, kwargs={"use_reloader": False}).start()
End-to-End Testing¶
Some testing use-cases might mean you want to temporarily expose a route via a pyngrok
tunnel to fully
validate a workflow. For example, an internal end-to-end tester, a step in a pre-deployment validation pipeline, or a
service that automatically updates a status page.
Whatever the case may be, extending unittest.TestCase
and adding your own fixtures that start the dev server and open a pyngrok
tunnel is relatively simple. This
snippet builds on the Flask example above, but it could be modified to work with other
frameworks.
import os
import signal
import unittest
import threading
from flask import request
from pyngrok import ngrok
from urllib import request
from server import create_app
class PyngrokTestCase(unittest.TestCase):
@classmethod
def start_dev_server(cls):
app = create_app()
def shutdown():
# Newer versions of Werkzeug and Flask don't provide this environment variable
if "werkzeug.server.shutdown" in request.environ:
request.environ.get("werkzeug.server.shutdown")()
else:
# Windows does not provide SIGKILL, go with SIGTERM then
sig = getattr(signal, "SIGKILL", signal.SIGTERM)
os.kill(os.getpid(), sig)
@app.route("/shutdown", methods=["POST"])
def route_shutdown():
shutdown()
return "", 204
threading.Thread(target=app.run).start()
return app
@classmethod
def stop_dev_server(cls):
req = request.Request("http://localhost:5000/shutdown", method="POST")
request.urlopen(req)
@classmethod
def setUpClass(cls):
# Ensure a tunnel is opened and webhooks initialized when the dev server is started
os.environ["USE_NGROK"] = True
app = cls.start_dev_server()
cls.base_url = app.config["BASE_URL"]
# ... Implement other initializes so you can assert against the inbound traffic through your tunnel
@classmethod
def tearDownClass(cls):
cls.stop_dev_server()
ngrok.kill()
Now, any test that needs to assert against responses through a pyngrok
tunnel can simply extend PyngrokTestCase
to inherit these fixtures. If you want the pyngrok
tunnel to remain open across numerous tests, it may be more
efficient to setup these fixtures at the suite or module level instead.
AWS Lambda (Local)¶
Lambdas deployed to AWS can be easily developed locally using pyngrok
and extending the
Flask example shown above. In addition to effortless local development, this gives you more flexibility when
writing tests, leveraging a CI, managing revisions, etc.
Let’s assume you have a file foo_GET.py
in your lambdas
module and, when deployed, it handles requests to
GET /foo
. Locally, you can use a Flask route as a shim to funnel requests to this same Lambda handler.
To start, add app.register_blueprint(lambda_routes.bp)
to server.py
from the example above. The create
lambda_routes.py
as shown below to handle the routing:
import json
from flask import Blueprint, request
from lambdas.foo_GET import lambda_function as foo_GET
bp = Blueprint("lambda_routes", __name__)
@bp.route("/foo")
def route_foo():
# This becomes the event in the Lambda handler
event = {
"someQueryParam": request.args.get("someQueryParam")
}
return json.dumps(foo_GET.lambda_handler(event, {}))
For a complete example of how you can leverage all these things together to rapidly develop, test,
and deploy AWS Lambda’s, check out the Air Quality Bot repository
and have a look at the Makefile
and devserver.py
.
Simple HTTP Server¶
Python’s http.server module also makes for a useful development
server. You can use pyngrok
to expose it to the web via a tunnel, as shown in server.py
here:
import os
from http.server import HTTPServer, BaseHTTPRequestHandler
from pyngrok import ngrok
port = os.environ.get("PORT", "80")
server_address = ("", port)
httpd = HTTPServer(server_address, BaseHTTPRequestHandler)
public_url = ngrok.connect(port).public_url
print(f"ngrok tunnel \"{public_url}\" -> \"http://127.0.0.1:{port}\"")
try:
# Block until CTRL-C or some other terminating event
httpd.serve_forever()
except KeyboardInterrupt:
print(" Shutting down server.")
httpd.socket.close()
You can then run this script to start the server.
NGROK_AUTHTOKEN=$NGROK_AUTHTOKEN python server.py
Simple TCP Server and Client¶
Here is an example of a simple TCP ping/pong server. It opens a local socket, uses ngrok
to tunnel to that
socket, then the client/server communicate via the publicly exposed address.
For this code to run, you’ll first need a reserved TCP address, which you obtain using
ngrok’s API. Set the HOST
and PORT
environment variables pointing to that reserved
address.
Now create server.py
with the following code:
import os
import socket
from pyngrok import ngrok
host = os.environ.get("HOST")
port = int(os.environ.get("PORT"))
# Create a TCP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Bind a local socket to the port
server_address = ("", port)
sock.bind(server_address)
sock.listen(1)
# Open a ngrok tunnel to the socket
public_url = ngrok.connect(port, "tcp", remote_addr=f"{host}:{port}").public_url
print(f"ngrok tunnel \"{public_url}\" -> \"tcp://127.0.0.1:{port}\"")
while True:
connection = None
try:
# Wait for a connection
print("\nWaiting for a connection ...")
connection, client_address = sock.accept()
print(f"... connection established from {client_address}")
# Receive the message, send a response
while True:
data = connection.recv(1024)
if data:
print("Received: {data}".format(data=data.decode("utf-8")))
message = "pong"
print(f"Sending: {message}")
connection.sendall(message.encode("utf-8"))
else:
break
except KeyboardInterrupt:
print(" Shutting down server.")
if connection:
connection.close()
break
sock.close()
In a terminal window, you can now start your socket server:
NGROK_AUTHTOKEN=$NGROK_AUTHTOKEN \
HOST="1.tcp.ngrok.io" PORT=12345 \
python server.py
It’s now waiting for incoming connections, so let’s write a client to connect to it and send it something.
Create client.py
with the following code:
import os
import socket
host = os.environ.get("HOST")
port = int(os.environ.get("PORT"))
# Create a TCP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Connect to the server with the socket via your ngrok tunnel
server_address = (host, port)
sock.connect(server_address)
print(f"Connected to {host}:{port}")
# Send the message
message = "ping"
print(f"Sending: {message}")
sock.sendall(message.encode("utf-8"))
# Await a response
data_received = 0
data_expected = len(message)
while data_received < data_expected:
data = sock.recv(1024)
data_received += len(data)
print("Received: {data}".format(data=data.decode("utf-8")))
sock.close()
In another terminal window, you can run your client:
HOST="1.tcp.ngrok.io" PORT=12345 \
python client.py
And that’s it! Data was sent and received from a socket via your ngrok
tunnel.