Spaces:
Paused
Paused
Switch default tunnel to bore (no token needed)
Browse files- Dockerfile +14 -1
- README.md +15 -13
- app/main.py +10 -11
- 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 **
|
| 18 |
-
|
|
|
|
| 19 |
|
| 20 |
-
> β οΈ **
|
| 21 |
-
>
|
| 22 |
|
| 23 |
---
|
| 24 |
|
| 25 |
## Deploy (5 minutes)
|
| 26 |
|
| 27 |
-
1. **
|
| 28 |
-
|
| 29 |
-
|
| 30 |
`Dockerfile`, `start.sh`, `README.md`, and the `app/` folder.
|
| 31 |
-
|
| 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 |
-
| `
|
|
|
|
|
|
|
| 37 |
| `POSTGRES_USER` / `POSTGRES_DB` | optional | Defaults: `demo` / `demo` |
|
| 38 |
-
|
| 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@
|
| 46 |
```
|
| 47 |
|
| 48 |
```python
|
| 49 |
import psycopg
|
| 50 |
-
conn = psycopg.connect("postgresql://demo:PASSWORD@
|
| 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 |
-
|
| 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
|
| 38 |
try:
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 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 "β
|
| 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
|
| 193 |
-
"Data is
|
| 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
|
|
|
|
|
|
|
|
|
|
| 83 |
if [ -n "${NGROK_AUTHTOKEN:-}" ]; then
|
| 84 |
-
echo "[start]
|
| 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 |
-
|
| 89 |
-
echo "[start]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) ----
|