VPS demo topology

Single-box demo: static AXIS dist + Flask Pro API (+ optional reverse proxy) without Cloudflare.

This page

VPS demo topology

Abstract

A VPS demo is the lowest-surprise production-like setup for Pine evaluation: one machine runs Flask (PYNE) and serves the static AXIS build. Optional nginx/caddy terminates TLS and reverse-proxies /run same-origin to avoid CORS.

No Worker required. Cloud storage and DO streams will not work without the Worker.

Conceptual model

Diagram

Rendering…

Interface surface

Processes

ProcessCommandPort
Build (CI or once)cd frontend && bun run build
StaticPORT=8081 python axis_pwa_server.py8081
APImake run / python -m backend.app5002

Same-origin proxy sketch (Caddy)

axis.example.com {
  handle /run* {
    reverse_proxy 127.0.0.1:5002
  }
  handle /optimize* {
    reverse_proxy 127.0.0.1:5002
  }
  handle {
    reverse_proxy 127.0.0.1:8081
  }
}

Then PWA endpoint = https://axis.example.com and the server engine posts to /run on the same host.

Production (split: Pages PWA + Hetzner API)

  • PWA — Cloudflare Pages custom domain https://axis.hoox.sh
  • PYNE Pro API — Hetzner VPS https://pynescript.online (Cloudflare orange-cloud → origin 204.168.138.51). nginx :443 reverse-proxies /run, /optimize, /ws/, /health, /lsp/, /datafeed/, … to gunicorn 127.0.0.1:5002
  • PWA Backend URL = https://pynescript.online (cross-origin; product CORS regex)
  • Cloudflare Pages previews (*.axis.pages.dev) use the same Backend URL — pyne must allow those Origins on /health and /run (always-on product regex; see CORS)

SSH alias: ssh pynescript (204.168.138.51). The previous combined VPS (ssh axis / 162.254.38.194) stays until decommission.

Update deploy (from a machine that can SSH to the VPS):

# 1. Ship latest main
git push origin main

# 2. On VPS (paths may vary — often /root/axis + rsync dist → PWA WorkingDirectory)
# Pro API (from pynescript checkout)
./scripts/deploy_vps.sh
# or:
ssh pynescript 'cd /root/pynescript && git pull --ff-only && systemctl restart pynescript-api'
# PWA: `bun run build` + wrangler pages deploy (axis.hoox.sh)

Local operator CLI for the Worker edge (separate from VPS static): axis deploy — see AXIS CLI.

Hardened network (UFW) + health checks

Production VPS pattern (verified on pynescript.online):

SurfaceBind / allowHealth URL
UFWdeny incoming default; allow 22/tcp, 80/tcp, 443/tcp
nginx0.0.0.0:443 TLS (+ :80 ACME)https://pynescript.online/health → proxy 127.0.0.1:5002/health
Pro API (gunicorn)127.0.0.1:5002 onlyloopback; not public
PWA staticCloudflare Pages (axis.hoox.sh)not on this VPS
fail2bansshd jail

Do not open UFW for 5002, 80, or 8787 on a public VPS. Direct probes to http://<vps-ip>:5002/health must time out if hardening is correct.

Workers Manager browser probes:

  • On product API host (pynescript.online), pyne Pro is probed via same-origin /health (nginx), not http://127.0.0.1:5002 (that would hit the client PC and always look “down”).
  • From CF Pages (axis.hoox.sh / *.axis.pages.dev) catalog publicEndpoint (https://pynescript.online) is used — never client loopback.
  • CF Worker health stays on https://worker.axis.hoox.sh/health (edge; independent of VPS UFW).

Minimal without TLS

  • Static :8081, Flask :5002
  • Prefer a hostname over raw IP when the page is HTTPS
  • Open firewall; accept CORS from static origin (configure Flask accordingly)

Internals

ConcernNotes
Pyodide offlineShip full dist including vendor + pyodide
Process supervisorsystemd units for flask + axis_pwa_server
UpdatesRebuild dist artifact; restart static only
SecretsFlask admin / pro keys separate from Worker

Worked example — systemd sketch

# /etc/systemd/system/axis-pwa.service
[Service]
WorkingDirectory=/opt/axis/frontend
Environment=PORT=8081
ExecStart=/usr/bin/python3 axis_pwa_server.py
Restart=on-failure

Pair with an existing backend unit for Flask.

Invariants & edge cases

  1. CPU/RAM — Pyodide is browser-side; VPS load is Flask evaluate concurrency.
  2. Do not expose open admin tokens.
  3. Disk — dist + wheels are tens of MB.
  4. Hybrid: VPS Flask as EXTERNAL_BACKEND for a CF Worker still works if VPS has a public URL.

Failure modes

IssueFix
Mixed contentHTTPS page calling HTTP Flask blocked
CORSPrefer same-origin proxy
502 on /runFlask crashed; check journalctl

See also