Exposing Jupyter & Model APIs: The Python Data Science Workflow with Pagekite
IT

Quick answer
Pagekite: The Python Localhost Tunnel for Data Science Workf: webhook testing answer
For local webhook testing, run your app locally, expose it with a public HTTPS tunnel, and paste the stable callback URL into the provider dashboard.
How do I test webhooks on localhost?
Start your local server, open a public HTTPS tunnel to that port, configure the provider webhook URL, and inspect events in your local logs.
Why does a stable webhook URL matter?
Stable URLs prevent provider dashboards from needing manual callback updates every time you restart a tunnel.
Data science and machine learning development happens predominantly on local machines or dedicated GPU workstations. Whether you’re iterating on exploratory analysis inside a Jupyter notebook, building a prototype with Streamlit, or serving predictions via FastAPI, your local environment is the primary workbench.
A recurring challenge shows up the moment you need to share that work. Demonstrating an interactive model to a remote stakeholder, testing an inbound webhook from a payment gateway, or letting a mobile app hit an endpoint running on your laptop usually means deploying to a remote server — and cloud deployment introduces friction: containerization overhead, cost, deployment delay, and configuration drift between environments.
This is the gap a localhost tunnel is built to close. While ngrok dominates general web development, Pagekite is a much older, still-maintained project worth knowing about specifically because its reference client is a single, dependency-light Python script rather than a compiled binary — a property that matters more than it sounds like it should in regulated or security-conscious environments.
The Local Development Friction in Modern Data Science
+-----------------------------------------------------------------------+
| Local Workstation |
| |
| +-------------------+ +--------------------+ +--------------+ |
| | JupyterLab / Notebook | | Streamlit / Gradio | | FastAPI / ML | |
| | (Port 8888) | | (Port 8501) | | (Port 8000) | |
| +---------+---------+ +---------+----------+ +-------+------+ |
| | | | |
+------------+------------------------+-------------------+-------------+
|
( Blocked by NAT / Firewall )
|
x
[ Public Internet / Clients ]
Three sharing bottlenecks show up constantly:
- Demonstrating interactive notebooks. A
.ipynbfile shared over email or GitHub only renders static output. Giving a client or PM a live, interactive session means exposing your running Jupyter server. - Testing webhooks and external callbacks. Slack bots, GitHub triggers, and Stripe/Twilio integrations all need a public URL that routes inbound POST requests straight to your machine.
- Remote API integration. Frontend engineers building against a locally hosted model endpoint need a reachable URL before the model is staged anywhere.
Manually provisioning an EC2 box, hand-rolling an SSH reverse tunnel, or installing a compiled binary that trips corporate application-whitelisting are all real friction points that a tunnel is meant to remove.
What Is Pagekite? Architecture of a Python-Powered Tunnel
Pagekite is an open-source tunneling project originally written by Bjarni Rúnar Einarsson and maintained under The Beanstalks Project ehf., an Icelandic company — the project has been running since around 2010, which makes it one of the older tools in the localhost-tunneling space, predating ngrok’s public launch. It creates a tunnel from a public relay server (a “front-end,” commonly hosted at pagekite.net) to a local service running behind NAT or a restrictive firewall.
PAGEKITE ARCHITECTURE
+------------------------+ +------------------------+
| Local Machine | | Pagekite Public Relay |
| | | (Front-end / Cloud) |
| +--------------------+ | | |
| | Local App | | | |
| | (Jupyter/FastAPI) | | | |
| +---------+----------+ | | |
| | Local HTTP | | |
| +---------v----------+ | Encrypted | |
| | Pagekite Client |<==============>| Public HTTP/HTTPS |
| | (pagekite.py) | | Tunnel | Front-end Listener |
| +--------------------+ | +-----------+------------+
+------------------------+ |
|
+--------v--------+
| Remote Client / |
| Web Browser |
+-----------------+
Key architectural characteristics
- Reference client is plain Python.
pagekite.pyis a single Python script with no compiled C extensions, Rust, or Go runtime required for the back-end/client role. Note one nuance: if you run your own front-end relay rather than using the hostedpagekite.netservice, terminating TLS on that relay needs OpenSSL and either a modern Python 3 or thepyOpenSSLmodule — so “zero dependencies” is really “zero dependencies for the common client use case.” - Python 3 is supported today. The current
pagekite.nethomepage documents installation withpython3 pagekite.py 80 yourname.pagekite.me. Some of the project’s older wiki pages (the “QuickStart Guide” for the 0.3.x/0.4.x line) still instruct readers to install Python 2.x — that’s stale documentation left over from the project’s early years, not a sign the tool itself is Python-2-only; Python 3 support landed as an explicit PyPagekite release years ago. - Protocol versatility. Beyond HTTP/HTTPS, Pagekite tunnels raw TCP, SSH, and other TCP-based services.
- Self-hosting is real and native. You can use the hosted relay at
pagekite.net, or run your own front-end (pagekitefront) inside your own infrastructure — the relay code itself is free software, not a paid-only feature gated behind a license key. - Built-in dynamic DNS. The client has native support for updating dynamic-DNS providers (dyndns.org, no-ip.com, or a custom HTTP(S) endpoint) so a public name keeps pointing at your current IP as it changes across networks.
- A wider ecosystem than just the Python script. Beyond
pagekite.py, the project also maintains libpagekite, a C implementation aimed at high-performance or embedded use, and upagekite, a MicroPython port intended for ESP32-class microcontrollers — relevant if your “local service” is closer to a sensor than a laptop.
Correcting the license: it’s AGPL, not GPLv2/Apache
pagekite.py itself is released under the GNU Affero General Public License (AGPL), copyright Bjarni Rúnar Einarsson and The Beanstalks Project ehf. Documentation is licensed CC BY-SA 3.0, and sample configuration files are public domain. The Apache-2.0-or-AGPL dual license you may see referenced belongs to libpagekite, the separate C library — not to the Python client most developers actually run. For a security review, this distinction matters: AGPL’s network-use clause is stricter than a permissive Apache license, and it’s the license that actually applies to the script running on your machine.
Python-Native Stack Advantages
| Feature | Benefit for ML / Data Teams |
|---|---|
| No compiled runtime to install | The client is one .py file — drop it in a venv, a Docker image, or next to your training scripts with no binary to vet |
| Code auditability | Plain, uncompiled Python source lets InfoSec review exactly what a tunnel does before approving it |
| Subprocess-level integration | Launch and manage the tunnel from inside a training or serving script using standard Python process libraries |
| Virtual environment friendly | The script itself has no packaging system to fight — it runs inside whatever venv/conda env already has Python 3 |
A correction worth flagging: there is no pip install pagekite
The original draft of this piece (and a number of other write-ups around the web) suggest pip install pagekite. As of this check, there is no actively maintained pagekite package on PyPI that corresponds to this tool — the closest name-match on PyPI is an unrelated static-site package called pagekit. The project’s own documentation consistently points to one of these instead:
# Download the script directly (the officially documented method)
curl -O https://pagekite.net/pk/pagekite.py
chmod +x pagekite.py
# Or, on Debian/Ubuntu, via the project's own apt repository
echo "deb http://pagekite.net/pk/deb/ pagekite main" | sudo tee -a /etc/apt/sources.list
sudo apt-get update && sudo apt-get install pagekite
Debian and Ubuntu also carry pagekite in their own official repositories (sudo apt install pagekite), though the project notes its own repo tends to ship newer releases than the distro-maintained package.
How to Expose a Jupyter Notebook with Pagekite
Exposing an interactive Jupyter environment to remote collaborators requires both connectivity and access control — an unauthenticated Jupyter session handed a public URL gives anyone who finds it full code-execution rights on your machine.
SECURE JUPYTER TUNNEL WORKFLOW
+----------------------+ +----------------------+ +----------------------+
| 1. Configure Jupyter | | 2. Launch Local Server| | 3. Create Pagekite |
| Set Strong Token |---->| Bind to 127.0.0.1 |---->| Encrypted Tunnel |
| or Password | | Port 8888 | | https://... |
+----------------------+ +----------------------+ +----------------------+
Step 1: Get the Pagekite client
curl -O https://pagekite.net/pk/pagekite.py
chmod +x pagekite.py
Step 2: Secure Jupyter before you tunnel anything
jupyter notebook --generate-config
jupyter notebook password
Or launch with an explicit token and bind to loopback only:
jupyter lab --ip=127.0.0.1 --port=8888 --NotebookApp.token='a_very_long_secure_random_token_12345'
Step 3: Launch the tunnel
python3 pagekite.py 8888 mynotebook.pagekite.me
The first run walks you through creating a free account (or signing in), and stores an authentication key locally in ~/.pagekite.rc. On success you’ll see terminal output confirming the local port is now reachable at https://mynotebook.pagekite.me/.
Step 4: Lock the tunnel down further, correctly
The earlier version of this guide invented flags like --allow=<ip> and --opt/basicauth=user:pass. Those aren’t real Pagekite options. The actual, documented access-control mechanism is a pair of + flags appended to the kite definition itself:
# Only allow a specific IP (or /24 subnet) to reach this kite
python3 pagekite.py 8888 mynotebook.pagekite.me +ip/203.0.113.45=ok
# Require HTTP Basic Auth in addition to (or instead of) Jupyter's own token
python3 pagekite.py 8888 mynotebook.pagekite.me +password/admin=ComplexPassword123!
Multiple +ip/ or +password/ flags can be stacked to allow more than one address or credential pair. Separately, pagekite.py has shipped a basic built-in request firewall since version 0.5 that blocks common attack paths (like /wp-admin/) by default; it can be turned off with --insecure globally or +insecure per kite, and turns itself off automatically once you’ve set up +password/ or +ip/ access control anyway.
Exposing FastAPI & Streamlit Model Endpoints
Scenario A: A Streamlit dashboard
streamlit run app.py --server.port 8501 --server.address 127.0.0.1
To force HTTPS on the public side, prefix the protocol onto the kite name itself — there’s no separate --service=https flag:
python3 pagekite.py 8501 https:my-analytics.pagekite.me
Scenario B: A FastAPI inference endpoint, launched programmatically
Instead of two terminal windows, you can spawn the tunnel as a subprocess alongside your ASGI server:
import subprocess
import time
import uvicorn
from fastapi import FastAPI
app = FastAPI(title="Local Machine Learning Inference API")
@app.get("/")
def read_root():
return {"status": "online", "model": "RandomForestClassifier_v2"}
@app.post("/predict")
def predict(features: dict):
processed_val = sum(features.values()) if features else 0
return {"prediction": processed_val * 1.5, "confidence": 0.94}
def start_pagekite_tunnel(port: int, subdomain: str):
"""Launches pagekite.py as a background process tied to the local port."""
cmd = ["python3", "pagekite.py", str(port), f"https:{subdomain}.pagekite.me"]
print(f"[*] Starting Pagekite tunnel on port {port}...")
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
time.sleep(2) # give the tunnel time to establish
print(f"[+] Tunnel online: https://{subdomain}.pagekite.me")
return process
if __name__ == "__main__":
PORT = 8000
SUBDOMAIN = "my-ml-model-api"
tunnel_process = start_pagekite_tunnel(port=PORT, subdomain=SUBDOMAIN)
try:
uvicorn.run(app, host="127.0.0.1", port=PORT)
finally:
print("[*] Terminating Pagekite process...")
tunnel_process.terminate()
Worth noting: this is process management, not a dedicated SDK — Pagekite doesn’t ship an official importable Python client library separate from the CLI script, so “programmatic integration” here means spawning and supervising the same script you’d run by hand, not calling into a purpose-built API.
Pagekite vs ngrok: A Data Science Comparison
| Metric | Pagekite | ngrok |
|---|---|---|
| Client language | Pure Python (client role) | Go |
| Client license | GNU AGPL (pagekite.py); libpagekite C library is Apache-2.0/AGPL dual-licensed | Closed-source, SaaS |
| Installation | curl the script, or apt/rpm package — no official PyPI package | Single precompiled binary |
| Self-hosting the relay | Native — run your own front-end, the relay code is free software | Not available on standard consumer/team plans |
| Custom domains | CNAME-based custom domains supported directly; true apex/root-domain support isn’t clearly documented by the project and shouldn’t be assumed | Subdomain custom domains via CNAME on the paid Pay-as-you-go plan; ngrok’s own docs state apex/root domains are not supported on any plan |
| Protocol support | HTTP, HTTPS, raw TCP, SSH | HTTP, HTTPS, TCP, TLS tunnels |
| Pricing model | Pay-what-you-want (suggested $3/month), free tier for non-commercial/FOSS use, subscription/team plans from $5.99/month, white-label bulk from $199.95/month for up to 500 devices | Free tier (limited), Hobbyist $10/month ($8/month billed annually), paid tiers scale up from there |
The comparative points that actually hold up
Binary vs. script. ngrok ships a compiled Go binary; Pagekite’s client is an inspectable Python script. In environments where application whitelisting blocks unsigned executables, that’s a genuine practical difference, license technicalities aside.
Self-hosting. If your data includes PII or regulated healthcare/financial records, Pagekite’s ability to run its own relay entirely inside your infrastructure is real and documented — this isn’t a paid add-on, it’s the same open-source relay code the hosted service runs.
Pricing shape, not just price. ngrok’s free and Hobbyist tiers are metered on bandwidth and endpoint count. Pagekite’s pricing is structured around a “pay what you want” model with a suggested minimum, plus flat monthly subscription tiers rather than per-GB overage charges — a genuinely different pricing philosophy worth knowing about before you assume “cheaper” or “more expensive” in either direction.
Security Best Practices for a Pagekite-Based Reverse Proxy
+-------------------------------------------------------------------+
| TUNNEL SECURITY LAYER STRATEGY |
| |
| [ Public Internet ] |
| | |
| v |
| +-------------------------------------------------------------+ |
| | Layer 1: Transport Encryption (prefix the kite with https:) | |
| +------------------------------+------------------------------+ |
| | |
| v |
| +-------------------------------------------------------------+ |
| | Layer 2: Access Control (+ip/ and +password/ flags) | |
| +------------------------------+------------------------------+ |
| | |
| v |
| +-------------------------------------------------------------+ |
| | Layer 3: Application Auth (Jupyter Token / API Keys) | |
| +------------------------------+------------------------------+ |
| | |
| v |
| [ Local Application / Notebook / FastAPI ] |
- Force HTTPS. Prefix the kite name:
python3 pagekite.py 8888 https:mynotebook.pagekite.me. - Restrict by IP where you can.
+ip/203.0.113.45=ok(single address) or+ip/203.0.113=ok(a /24 netblock). - Add HTTP Basic Auth at the tunnel layer.
+password/admin=ComplexPassword123!— useful as a second factor in front of an app that has weak or no auth of its own. - Run the local service unprivileged. Keep Jupyter/FastAPI processes off root, and keep them out of directories containing
.envfiles or SSH keys — Pagekite forwards whatever the local port serves, so process hygiene on the local side still matters.
Handling Large Data Payloads
If FastAPI is returning bulky JSON or image payloads, enable response compression before the data crosses the tunnel:
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
app = FastAPI()
# Note the capitalization: it's GZipMiddleware, not GzipMiddleware
app.add_middleware(GZipMiddleware, minimum_size=500)
Maintaining Persistent Background Tunnels
nohup python3 pagekite.py --logfile=/var/log/pagekite.log 8888 mynotebook.pagekite.me &
For anything you want surviving reboots, wrap this in a systemd unit rather than relying on nohup alone.
Troubleshooting Common Connection Issues
| Symptom | Likely Cause | Fix |
|---|---|---|
| “Connection Refused” | Local service isn’t actually listening on the port you specified | Confirm with curl http://127.0.0.1:PORT or netstat before blaming the tunnel |
| “Subdomain Unavailable” | The requested name.pagekite.me is already taken | Choose a different prefix, or check that your existing kite credentials are in ~/.pagekite.rc |
| Slow responses | Payload size or NAT/MTU mismatch | Enable response compression (see above) or reduce payload size |
Integrating Pagekite into MLOps Pipelines
MLOps INTEGRATION PIPELINE
+-------------------+ +--------------------+ +---------------------+
| GitHub Actions / | | Spawn Ephemeral | | Run Pagekite Tunnel |
| Local CI Runner |---->| Model Container |---->| To Expose Webhook |
+-------------------+ +--------------------+ +----------+----------+
|
v
+-------------------+ +--------------------+ +---------------------+
| Teardown Tunnel | | Assert Webhook |<----| Trigger External |
| & Report Results |<----| Payload Delivery | | Service Test |
+-------------------+ +--------------------+ +---------------------+
In end-to-end integration tests that need to verify an external service can actually reach an internal webhook handler, spinning up a short-lived, uniquely-named kite (test-run-$GITHUB_RUN_ID.pagekite.me) inside the test runner, triggering the external call, asserting the payload arrived, and tearing the tunnel down again avoids paying for a permanent staging environment solely for webhook verification.
Beyond the Python Client: libpagekite and upagekite
Two lesser-known parts of the Pagekite project are worth knowing about if your work extends past notebooks and APIs into embedded or resource-constrained devices:
- libpagekite is a from-scratch C implementation of the same protocol, aimed at high-performance or embedded deployments where spinning up a Python interpreter isn’t practical.
- upagekite is a MicroPython port targeting ESP32-class microcontrollers, letting a sensor or IoT device fly its own kite without a general-purpose OS underneath it.
If you’re already using Pagekite for a data-science API and later need to pull telemetry off an ESP32 board, that’s the same underlying front-end infrastructure, not a different vendor.
Is Pagekite Still a Good Fit in 2026?
Worth being honest about, in the interest of not overselling this: pagekite.net’s own blog hasn’t posted a public update since October 2021, and the GitHub repository for the Python client carries a standing note that it’s “under active development” and “may at times be somewhat unstable.” That’s a slower cadence than ngrok or newer entrants like zrok or Pinggy, both of which ship changelog entries regularly.
That said, as of this check the core service is still live, the download and signup flow works, Python 3 is the documented path, and the pricing/FAQ pages are current — this reads less like an abandoned project and more like mature, low-churn infrastructure maintained by a small team. For a corporate environment where an auditable, self-hostable, script-based tunnel is the actual requirement — rather than the newest feature set — that stability profile can be a reasonable trade-off. For teams that want active support channels, a polished dashboard, or frequent feature releases, the newer tools already covered elsewhere in this series are a closer fit.
Streamlining the Python Tunneling Stack
A localhost tunnel solves a real bottleneck: bridging local development and the public web without standing up cloud infrastructure just to demo something. Pagekite’s pitch — a script-based, self-hostable, AGPL-licensed client backed by an Icelandic operator running since 2010 — is genuinely different from ngrok’s closed-source SaaS model, and the difference is real even after correcting the specific claims that didn’t hold up. Whether it’s the right tool depends more on whether your priority is auditability and self-hosting or feature velocity and polish.
Editorial changelog (fact-checked Sept 6, 2026)
Corrections made to the original draft:
- License: corrected from an invented “GPLv2/Apache” to the actual GNU AGPL under which
pagekite.pyis released (Apache-2.0/AGPL dual license applies only to the separate C library, libpagekite, not the Python client). - Installation: removed the fabricated
pip install pagekite— there is no actively maintained PyPI package for this tool; replaced with the project’s actual documented install paths (direct script download via curl, the project’s own apt repository, or distro packages). - Python 2 vs 3: added the nuance that some of the project’s own older wiki pages still reference Python 2.x, but the current homepage and PyPagekite release notes confirm Python 3 support.
- CLI syntax for HTTPS: removed the invented
--service=httpsflag; replaced with the real mechanism of prefixing the protocol onto the kite name (e.g.,https:name.pagekite.me). - CLI syntax for security: removed the invented
--allow=<ip>and--opt/basicauth=user:passflags; replaced with the real, documented+ip/and+password/per-kite flags, and added the real detail about the built-in request firewall since v0.5 and the--insecure/+insecureopt-out. - Code correction: fixed
GzipMiddlewareto the correct FastAPI/Starlette class name,GZipMiddleware. - Added real pricing: the original draft had no pricing section; added Pagekite’s actual pay-what-you-want model, subscription tiers, and white-label bulk pricing from pagekite.net, and tightened the ngrok side of the comparison to match previously verified figures for this series (no session timeout on ngrok’s free tier; custom domains are subdomain-only via CNAME on ngrok’s paid tier, with no apex/root domain support on any plan).
- Added company/creator attribution: named Bjarni Rúnar Einarsson and The Beanstalks Project ehf. (Iceland) as the project’s origin, which the original draft omitted.
- Added ecosystem context: libpagekite © and upagekite (MicroPython/ESP32) weren’t mentioned in the original draft.
- Added an honest maintenance-status assessment: noted the project’s public blog has been quiet since October 2021 and the GitHub repo’s own stability disclaimer, while confirming the service and downloads are still live and current as of this check.
- Softened an unverified claim: the original comparison table asserted Pagekite has full custom apex/root domain support; no documentation was found to substantiate that beyond CNAME-based custom domains, so the claim was walked back to “not clearly documented” rather than presented as confirmed.
- Removed the Meta Description line, per standard formatting for this series.
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Comments
Post a Comment