imkrish commited on
Commit
8dec6cd
Β·
verified Β·
1 Parent(s): 8003382

Switch default tunnel to bore (no token needed)

Browse files
Files changed (4) hide show
  1. Dockerfile +14 -1
  2. README.md +15 -13
  3. app/main.py +10 -11
  4. start.sh +28 -4
Dockerfile CHANGED
@@ -8,7 +8,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
8
  curl ca-certificates bash \
9
  && rm -rf /var/lib/apt/lists/*
10
 
11
- # ngrok v3 (gives us a public TCP endpoint for Postgres) β€” pick the right arch
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  RUN ARCH="$(dpkg --print-architecture)" \
13
  && curl -fsSL "https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-${ARCH}.tgz" \
14
  -o /tmp/ngrok.tgz \
 
8
  curl ca-certificates bash \
9
  && rm -rf /var/lib/apt/lists/*
10
 
11
+ # bore β€” open-source TCP tunnel, no signup (default). Arch-aware.
12
+ RUN ARCH="$(dpkg --print-architecture)" \
13
+ && case "$ARCH" in \
14
+ amd64) BARCH=x86_64 ;; \
15
+ arm64) BARCH=aarch64 ;; \
16
+ *) BARCH=x86_64 ;; \
17
+ esac \
18
+ && curl -fsSL "https://github.com/ekzhang/bore/releases/download/v0.6.0/bore-v0.6.0-${BARCH}-unknown-linux-musl.tar.gz" \
19
+ -o /tmp/bore.tgz \
20
+ && tar -xzf /tmp/bore.tgz -C /usr/local/bin \
21
+ && rm /tmp/bore.tgz \
22
+ && chmod +x /usr/local/bin/bore
23
+
24
+ # ngrok v3 β€” optional alternative tunnel (used only if NGROK_AUTHTOKEN is set)
25
  RUN ARCH="$(dpkg --print-architecture)" \
26
  && curl -fsSL "https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-${ARCH}.tgz" \
27
  -o /tmp/ngrok.tgz \
README.md CHANGED
@@ -14,40 +14,42 @@ A single Docker container that runs **PostgreSQL** + a small **web UI** that han
14
  **public connection URL** to drop into any demo project that needs a Postgres database.
15
 
16
  Because Hugging Face Spaces only exposes one HTTP port (no raw TCP), the container opens an
17
- outbound **ngrok TCP tunnel** so Postgres is reachable from anywhere with a normal
18
- `postgresql://…` URL and any client/ORM.
 
19
 
20
- > ⚠️ **Ephemeral & throwaway.** Data is wiped on every restart/rebuild, and the public URL
21
- > changes each time. Great for demos; do not use for anything you need to keep.
22
 
23
  ---
24
 
25
  ## Deploy (5 minutes)
26
 
27
- 1. **Get a free ngrok authtoken** β€” sign up at <https://dashboard.ngrok.com>, copy *Your Authtoken*.
28
- 2. **Create a Space** β†’ New Space β†’ **SDK: Docker** β†’ (recommended) **Visibility: Private**.
29
- 3. **Push these files** to the Space repo (or upload them in the web UI):
30
  `Dockerfile`, `start.sh`, `README.md`, and the `app/` folder.
31
- 4. **Add Space secrets** (Settings β†’ *Variables and secrets*):
32
  | Secret | Required | Purpose |
33
  |---|---|---|
34
- | `NGROK_AUTHTOKEN` | βœ… | Opens the public TCP tunnel for Postgres |
35
  | `APP_PASSWORD` | recommended | Locks the web UI (Basic auth, user `admin`) so only you see the URL |
36
- | `POSTGRES_PASSWORD` | optional | Fixed DB password (otherwise one is generated per boot) |
 
 
37
  | `POSTGRES_USER` / `POSTGRES_DB` | optional | Defaults: `demo` / `demo` |
38
- 5. The Space builds and starts. Open it β†’ the UI shows your **connection URL** + a Copy button.
39
 
40
  ## Use it
41
 
42
  Open the Space, copy the `postgresql://…` URL, and use it anywhere:
43
 
44
  ```bash
45
- psql "postgresql://demo:PASSWORD@0.tcp.ngrok.io:12345/demo"
46
  ```
47
 
48
  ```python
49
  import psycopg
50
- conn = psycopg.connect("postgresql://demo:PASSWORD@0.tcp.ngrok.io:12345/demo")
51
  ```
52
 
53
  Works the same with SQLAlchemy, Prisma, Drizzle, node-postgres, etc.
 
14
  **public connection URL** to drop into any demo project that needs a Postgres database.
15
 
16
  Because Hugging Face Spaces only exposes one HTTP port (no raw TCP), the container opens an
17
+ outbound **TCP tunnel** so Postgres is reachable from anywhere with a normal `postgresql://…`
18
+ URL and any client/ORM. The default tunnel is **[bore](https://github.com/ekzhang/bore)** β€”
19
+ open-source, **no signup, no token**. Set `NGROK_AUTHTOKEN` to use ngrok instead.
20
 
21
+ > ⚠️ **Throwaway by default.** The public URL changes on every restart. Data is **backed up**
22
+ > (see Persistence) but treat the cluster itself as disposable.
23
 
24
  ---
25
 
26
  ## Deploy (5 minutes)
27
 
28
+ 1. **Create a Space** β†’ New Space β†’ **SDK: Docker** β†’ **Visibility: Public** (so keep-alive can
29
+ reach it; the UI is still password-locked).
30
+ 2. **Push these files** to the Space repo (or upload them in the web UI):
31
  `Dockerfile`, `start.sh`, `README.md`, and the `app/` folder.
32
+ 3. **Add Space secrets** (Settings β†’ *Variables and secrets*):
33
  | Secret | Required | Purpose |
34
  |---|---|---|
 
35
  | `APP_PASSWORD` | recommended | Locks the web UI (Basic auth, user `admin`) so only you see the URL |
36
+ | `HF_TOKEN` + `HF_BACKUP_REPO` | for backups | Off-Space dumps to a private HF Dataset (see Persistence) |
37
+ | `POSTGRES_PASSWORD` | recommended | Fixed DB password (otherwise one is generated per boot) |
38
+ | `NGROK_AUTHTOKEN` | optional | Use ngrok instead of bore for the tunnel |
39
  | `POSTGRES_USER` / `POSTGRES_DB` | optional | Defaults: `demo` / `demo` |
40
+ 4. The Space builds and starts. Open it β†’ the UI shows your **connection URL** + a Copy button.
41
 
42
  ## Use it
43
 
44
  Open the Space, copy the `postgresql://…` URL, and use it anywhere:
45
 
46
  ```bash
47
+ psql "postgresql://demo:PASSWORD@bore.pub:26134/demo"
48
  ```
49
 
50
  ```python
51
  import psycopg
52
+ conn = psycopg.connect("postgresql://demo:PASSWORD@bore.pub:26134/demo")
53
  ```
54
 
55
  Works the same with SQLAlchemy, Prisma, Drizzle, node-postgres, etc.
app/main.py CHANGED
@@ -1,10 +1,10 @@
1
  import os
 
2
  import time
3
  import secrets
4
  import threading
5
  from datetime import datetime, timedelta
6
 
7
- import httpx
8
  import psycopg
9
  import gradio as gr
10
 
@@ -22,7 +22,7 @@ PG_PORT = os.environ.get("PGPORT", "5432")
22
  APP_PASSWORD = os.environ.get("APP_PASSWORD", "")
23
  APP_USER = os.environ.get("APP_USER", "admin")
24
 
25
- NGROK_API = "http://127.0.0.1:4040/api/tunnels"
26
 
27
  BOOT = datetime.now()
28
  HISTORY: list[tuple[str, bool, bool]] = [] # (HH:MM:SS, postgres_up, tunnel_up)
@@ -34,13 +34,12 @@ KEEPALIVE = {"count": 0, "last": None, "src": "β€”"}
34
 
35
  # ---------------------------------------------------------------- helpers ----
36
  def get_tunnel():
37
- """Return (host, port) of the public ngrok TCP tunnel, or None."""
38
  try:
39
- r = httpx.get(NGROK_API, timeout=3)
40
- for t in r.json().get("tunnels", []):
41
- if t.get("proto") == "tcp":
42
- host, port = t["public_url"].split("://", 1)[1].split(":")
43
- return host, port
44
  except Exception:
45
  return None
46
  return None
@@ -160,7 +159,7 @@ async def connection(_: None = Depends(require_auth)):
160
  def snapshot():
161
  tunnel = get_tunnel()
162
  up = pg_ok()
163
- url = build_url(tunnel) or "β€” no public tunnel yet (set NGROK_AUTHTOKEN) β€”"
164
  host, port = tunnel if tunnel else ("β€”", "β€”")
165
 
166
  samples = len(HISTORY)
@@ -189,8 +188,8 @@ def snapshot():
189
  with gr.Blocks(title="Remote Postgres β€” Uptime Watcher", theme=gr.themes.Soft()) as demo:
190
  gr.Markdown("# 🐘 Remote Postgres β€” Uptime Watcher")
191
  gr.Markdown(
192
- "Throwaway Postgres for demo projects, exposed over an ngrok TCP tunnel. "
193
- "Data is **ephemeral** and the URL **changes on every restart**."
194
  )
195
  status_box = gr.Markdown()
196
  url_box = gr.Textbox(
 
1
  import os
2
+ import json
3
  import time
4
  import secrets
5
  import threading
6
  from datetime import datetime, timedelta
7
 
 
8
  import psycopg
9
  import gradio as gr
10
 
 
22
  APP_PASSWORD = os.environ.get("APP_PASSWORD", "")
23
  APP_USER = os.environ.get("APP_USER", "admin")
24
 
25
+ TUNNEL_FILE = "/tmp/tunnel.json" # written by start.sh (bore or ngrok)
26
 
27
  BOOT = datetime.now()
28
  HISTORY: list[tuple[str, bool, bool]] = [] # (HH:MM:SS, postgres_up, tunnel_up)
 
34
 
35
  # ---------------------------------------------------------------- helpers ----
36
  def get_tunnel():
37
+ """Return (host, port) of the public TCP tunnel from /tmp/tunnel.json, or None."""
38
  try:
39
+ with open(TUNNEL_FILE) as f:
40
+ d = json.load(f)
41
+ if d.get("host") and d.get("port"):
42
+ return d["host"], str(d["port"])
 
43
  except Exception:
44
  return None
45
  return None
 
159
  def snapshot():
160
  tunnel = get_tunnel()
161
  up = pg_ok()
162
+ url = build_url(tunnel) or "β€” tunnel starting… refresh in a few seconds β€”"
163
  host, port = tunnel if tunnel else ("β€”", "β€”")
164
 
165
  samples = len(HISTORY)
 
188
  with gr.Blocks(title="Remote Postgres β€” Uptime Watcher", theme=gr.themes.Soft()) as demo:
189
  gr.Markdown("# 🐘 Remote Postgres β€” Uptime Watcher")
190
  gr.Markdown(
191
+ "Throwaway Postgres for demo projects, exposed over a TCP tunnel (bore). "
192
+ "Data is backed up, but the URL **changes on every restart**."
193
  )
194
  status_box = gr.Markdown()
195
  url_box = gr.Textbox(
start.sh CHANGED
@@ -79,14 +79,38 @@ if [ "$FRESH_INIT" = "1" ]; then
79
  python /app/backup.py restore || echo "[start] no restore performed"
80
  fi
81
 
82
- # ---- expose Postgres publicly via ngrok TCP tunnel ----
 
 
 
83
  if [ -n "${NGROK_AUTHTOKEN:-}" ]; then
84
- echo "[start] Configuring ngrok and opening TCP tunnel for port $PGPORT"
85
  ngrok config add-authtoken "$NGROK_AUTHTOKEN" >/dev/null 2>&1 || true
86
  ngrok tcp "$PGPORT" --log=stdout > "$HOME/ngrok.log" 2>&1 &
 
 
 
 
 
 
 
 
 
 
87
  else
88
- echo "[start] NGROK_AUTHTOKEN not set β€” Postgres is only reachable INSIDE the Space."
89
- echo "[start] Add a free ngrok authtoken as a Space secret to get a public URL."
 
 
 
 
 
 
 
 
 
 
 
90
  fi
91
 
92
  # ---- keep the Space awake (free Spaces sleep after 48h with NO http traffic) ----
 
79
  python /app/backup.py restore || echo "[start] no restore performed"
80
  fi
81
 
82
+ # ---- expose Postgres publicly via a TCP tunnel ----
83
+ # Default: bore (no signup). If NGROK_AUTHTOKEN is set, use ngrok instead.
84
+ # Whichever runs writes the public endpoint to /tmp/tunnel.json for the web app.
85
+ rm -f /tmp/tunnel.json
86
  if [ -n "${NGROK_AUTHTOKEN:-}" ]; then
87
+ echo "[start] Tunnel: ngrok (NGROK_AUTHTOKEN present)"
88
  ngrok config add-authtoken "$NGROK_AUTHTOKEN" >/dev/null 2>&1 || true
89
  ngrok tcp "$PGPORT" --log=stdout > "$HOME/ngrok.log" 2>&1 &
90
+ ( set +e
91
+ for _ in $(seq 1 30); do
92
+ pub="$(curl -fsS localhost:4040/api/tunnels 2>/dev/null | grep -oE 'tcp://[^"]+' | head -1)"
93
+ if [ -n "$pub" ]; then
94
+ hp="${pub#tcp://}"
95
+ printf '{"host":"%s","port":"%s","provider":"ngrok"}' "${hp%%:*}" "${hp##*:}" > /tmp/tunnel.json
96
+ echo "[start] ngrok tunnel up at ${hp}"; break
97
+ fi
98
+ sleep 1
99
+ done ) &
100
  else
101
+ BORE_HOST="${BORE_HOST:-bore.pub}"
102
+ echo "[start] Tunnel: bore -> ${BORE_HOST} (no token needed)"
103
+ bore local "$PGPORT" --to "$BORE_HOST" > "$HOME/bore.log" 2>&1 &
104
+ ( set +e
105
+ for _ in $(seq 1 30); do
106
+ port="$(grep -aoE 'listening at [^[:space:]]+:[0-9]+' "$HOME/bore.log" 2>/dev/null \
107
+ | grep -oE '[0-9]+$' | tail -1)"
108
+ if [ -n "$port" ]; then
109
+ printf '{"host":"%s","port":"%s","provider":"bore"}' "$BORE_HOST" "$port" > /tmp/tunnel.json
110
+ echo "[start] bore tunnel up at ${BORE_HOST}:${port}"; break
111
+ fi
112
+ sleep 1
113
+ done ) &
114
  fi
115
 
116
  # ---- keep the Space awake (free Spaces sleep after 48h with NO http traffic) ----