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
Rendering…
Interface surface
Processes
| Process | Command | Port |
|---|---|---|
| Build (CI or once) | cd frontend && bun run build | — |
| Static | PORT=8081 python axis_pwa_server.py | 8081 |
| API | make run / python -m backend.app | 5002 |
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 → origin204.168.138.51). nginx:443reverse-proxies/run,/optimize,/ws/,/health,/lsp/,/datafeed/, … to gunicorn127.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/healthand/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):
| Surface | Bind / allow | Health URL |
|---|---|---|
| UFW | deny incoming default; allow 22/tcp, 80/tcp, 443/tcp | — |
| nginx | 0.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 only | loopback; not public |
| PWA static | Cloudflare Pages (axis.hoox.sh) | not on this VPS |
| fail2ban | sshd 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), nothttp://127.0.0.1:5002(that would hit the client PC and always look “down”). - From CF Pages (
axis.hoox.sh/*.axis.pages.dev) catalogpublicEndpoint(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
| Concern | Notes |
|---|---|
| Pyodide offline | Ship full dist including vendor + pyodide |
| Process supervisor | systemd units for flask + axis_pwa_server |
| Updates | Rebuild dist artifact; restart static only |
| Secrets | Flask 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
- CPU/RAM — Pyodide is browser-side; VPS load is Flask evaluate concurrency.
- Do not expose open admin tokens.
- Disk — dist + wheels are tens of MB.
- Hybrid: VPS Flask as
EXTERNAL_BACKENDfor a CF Worker still works if VPS has a public URL.
Failure modes
| Issue | Fix |
|---|---|
| Mixed content | HTTPS page calling HTTP Flask blocked |
| CORS | Prefer same-origin proxy |
| 502 on /run | Flask crashed; check journalctl |