Self-hosted WebRTC signaling, auth, and session broker for Ponter remote terminal access
1.7K
Docker image for the self-hosted backend and WebRTC signaling server of Ponter — a remote access platform with a multi-shell remote terminal over peer-to-peer WebRTC DataChannels.
This image packages the @ponter/server Node.js application (apps/server) only. It does not contain the web UI, and the native Rust agent (ponter-agent) is not distributed as a Docker image.
Generate two secrets (32+ characters each), then run the container:
openssl rand -base64 48 # use for JWT_SECRET
openssl rand -base64 48 # use for REFRESH_TOKEN_SECRET
docker run -d \
--name ponter-server \
-p 8787:8787 \
-v ponter-data:/app/data \
-e JWT_SECRET="<32+ random characters>" \
-e REFRESH_TOKEN_SECRET="<32+ random characters>" \
-e CORS_ORIGIN="https://your-web-app.example.com" \
ngotuananh2101/ponter:latest
-p 8787:8787 — HTTP API and WebSocket signaling (same port).-v ponter-data:/app/data — persists the SQLite database across restarts.JWT_SECRET / REFRESH_TOKEN_SECRET — required; token operations return HTTP 500 without them.CORS_ORIGIN — set it to your web app origin (see the warning below).Verify the container is healthy:
curl http://localhost:8787/health
# {"status":"ok"}
The /health endpoint answers HTTP 200 without authentication.
| Port | 8787 — HTTP (/health, /api/*) and WebSocket upgrades (/api/ws/agent, /api/ws/browser) |
| Volume | /app/data — SQLite database (default /app/data/remote.db) |
| Base image | node:24-alpine; the server process runs as the unprivileged node user |
| Entrypoint | Starts as root only to chown /app/data (fixes bind-mount permissions), then drops to node via su-exec |
| Variable | Required | Default | Description |
|---|---|---|---|
PORT | No | 8080 (code); the image sets 8787 | HTTP + WebSocket listen port |
DATABASE_PATH | No | :memory: (code); the image sets /app/data/remote.db | SQLite database path |
JWT_SECRET | Yes | — | Signs access tokens and browser WebSocket tickets |
REFRESH_TOKEN_SECRET | Yes | — | Signs refresh tokens |
CORS_ORIGIN | No | * | Comma-separated origin allowlist for HTTP CORS and the browser WebSocket Origin check |
JWT_EXPIRES_IN | No | 900 (15m) | Access token TTL. Requires an explicit unit — s, m, h or d (e.g. 15m, 1h, 900s). A value without a unit is ignored and the default is used |
REFRESH_TOKEN_EXPIRES_IN | No | 604800 (7d) | Refresh token TTL. Same unit requirement |
TURN_SECRET | No | — | Coturn shared secret for minting time-limited TURN credentials |
TURN_URL | No | — | TURN server advertised to peers. Without it (and TURN_SECRET) the server falls back to public Google STUN |
STUN_URL | No | stun:stun.l.google.com:19302 | STUN entry used when TURN is configured |
CORS_ORIGIN warning. With CORS_ORIGIN=* (or unset), the browser WebSocket upgrade accepts any Origin, which disables the Cross-Site WebSocket Hijacking defence. Set it to your web app origin in production.
NODE_ENV and LOG_LEVEL are not read by the server; the image already sets NODE_ENV=production. DOMAIN, CLOUDFLARE_TUNNEL_TOKEN and DOCKERHUB_IMAGE are compose-level variables (Caddy, cloudflared, image selection), not read by the server process.
This image is one of three components:
apps/web) — a Vue 3 SPA deployed separately (e.g. Cloudflare Pages or any static host) and pointed at this server via VITE_API_URL.apps/agent) — the ponter-agent native binary that runs on the machines you want to reach, built from source with Cargo (cargo build --release --manifest-path apps/agent/Cargo.toml); see the Agent Setup Guide. It is not a Docker image.The repository ships ready-made compose setups in docker/:
docker-compose.tunnel.yml — homelab behind a Cloudflare Tunnel (no inbound ports to open).docker-compose.prod.yml — VPS with Caddy (automatic HTTPS) + Coturn.docker-compose.nginx.yml — VPS with an existing host Nginx + Coturn.They pull this image through the DOCKERHUB_IMAGE variable:
cd docker
cp .env.example .env
# edit .env: set JWT_SECRET, REFRESH_TOKEN_SECRET, DOMAIN, TURN_SECRET, and:
# DOCKERHUB_IMAGE=ngotuananh2101/ponter
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
See the Deployment Guide for the full walkthrough (tunnel, prod and nginx variants).
| Tag | Meaning |
|---|---|
latest | The most recent image manually published from the main branch via the Docker Publish workflow |
sha-<short> | Every publish run, tagged with the 7-character commit SHA (e.g. sha-553d3cd) — use it to pin a build or roll back |
All tags are multi-arch manifests (linux/amd64 + linux/arm64 in one image, built on native runners without QEMU). The compressed image is roughly 280 MB on amd64 and 270 MB on arm64.
/api/ws/agent with their agent credential; browsers mint a single-use ticket with a 15-second TTL via POST /api/ws/ticket and connect to /api/ws/browser. REST polling (/api/signal/poll/:sessionId) remains as a fallback.foreign_keys=ON, busy_timeout=5000); the schema is created at startup. The database is opened before the server starts listening, so an unusable DATABASE_PATH fails fast instead of failing on the first request.stop_grace_period: 15s so a redeploy does not look like a network drop to connected clients.Content type
Image
Digest
sha256:78e78f592…
Size
171.4 MB
Last updated
3 days ago
docker pull ngotuananh2101/ponter