Skip to main content
Pylon runs as a Rust binary (plus Bun, which runs your TypeScript and SSR) behind a TLS-terminating reverse proxy. This page covers the supported self-hosted deploy shapes plus the experimental Workers path. If you’d rather not run any of this yourself, Pylon Cloud hosts the same binary.

Required environment

Optional:
Hard requirements that fail to start:
  • PYLON_CORS_ORIGIN=* in non-dev mode is refused
  • In non-dev mode, the CORS gate refuses to start with no trusted origins — declare in manifest.auth.trustedOrigins or set PYLON_CORS_ORIGIN
  • PYLON_DEV_MODE=true with PYLON_ADMIN_TOKEN unset is refused
  • /api/__test__/reset is disabled unless dev + in-memory + loopback

Ports

Pylon uses up to four adjacent ports, depending on which transports you enable: Your reverse proxy needs to forward all relevant ports. For deployments with a single public port (Fly.io, Cloud Run), path-route the WebSocket/SSE/shard upgrades through that one port at the proxy (see the Caddy/nginx examples below) and point the client’s sync wsUrl at the public origin — there’s no server-side WS-URL env var; the client is told where to connect via its transport config.

Shape 1: single VPS (SSH + systemd)

Simplest and cheapest. The pylon binary plus Bun, one systemd unit, one reverse proxy. pylon deploy --target systemd generates this unit (plus step-by-step install commands) for your app; the shape looks like:
Reverse proxy (Caddy or nginx) forwards :443:4321 plus WebSocket upgrades for /ws:4322, /events (SSE) → :4323, and shard WS → :4324. pylon deploy --target systemd emits a matching nginx or Caddy reverse-proxy config alongside the unit file (generated by crates/runtime/src/tls.rs).
Backups: cron pylon backup /var/backups/pylon/$(date +%F) nightly. Test restore quarterly per the test at crates/runtime/tests/backup_restore.rs.

Caddy example

Shape 2: AWS ECS + Aurora (Terraform)

deploy/terraform/modules/pylon/ provisions:
  • ECS Fargate service (0.25 vCPU, 512 MB) ~$9/mo
  • Aurora Serverless v2 (0.5–2 ACU) ~$15/mo minimum
  • ALB with TLS + WebSocket routing
  • CloudFront CDN + Route53 DNS
Minimum bill: ~$25/mo for a production deployment. Compiled with --features postgres-live and DATABASE_URL=postgres://....

Shape 3: AWS via SST (TypeScript IaC)

For TypeScript-first infrastructure, SST v3 provisions the same AWS shape from a single sst.config.ts. Same components (Aurora, Fargate, ALB, EFS, CloudFront), one CLI for deploys + secrets + per-PR preview environments.
See Deploy with SST for the full walkthrough — secrets, custom domains, multi-port load balancer config, EFS vs S3 storage, horizontal scaling.

Shape 4: Cloudflare Workers (edge, experimental)

crates/workers/ builds a WASM bundle (worker-build --release) that runs on Workers with a D1 binding. See crates/workers/README.md for current limitations. Scale-to-zero: idle apps cost $0. Cost rises with actual request volume. See Workers costs. Realtime shards (tick-based sims) are not yet supported on Workers — they need persistent state that Workers-only can’t hold efficiently. Use shape 1 or 2 for game shards.

Shape 5: Pylon Cloud

Done. See Cloud for full details.

Shape 6: local dev

Starts on port 4321 with PYLON_DEV_MODE=true defaults. Studio at /studio, hot-reload, permissive CORS. Not for production.

Health checks

  • GET /health returns 200 with {"status":"ok","uptime_seconds":N}
  • GET /metrics returns Prometheus text when Accept: text/plain
  • GET /readyz checks DB connectivity
Hook these into your load balancer — unhealthy instances should drain.

Shutdown and rolling deploys

SIGTERM terminates the process. The runtime does not currently install a signal handler, so SIGTERM does not run an in-process drain — the drain path exists in the runtime but isn’t yet wired to a signal. Two things make an abrupt stop safe in practice:
  • SQLite (WAL mode) is crash-consistent. An abrupt stop won’t corrupt the database; the next start recovers the WAL. There’s no “flush on exit” to lose.
  • Drain at the load balancer, not in the process. For rolling deploys with zero dropped requests: stop routing new traffic to the old instance, wait out your load balancer’s connection-drain / deregistration delay so in-flight requests finish, then stop the process.
Rolling deploy sequence: start the new instance, let the load balancer health-check promote it, stop routing traffic to the old one, wait for the drain window, then SIGTERM it. (PYLON_DRAIN_SECS, default 10s, bounds the drain loop for the paths that do invoke the runtime’s shutdown — e.g. programmatic shutdown; it is not a SIGTERM grace period.)

Native clients (iOS, macOS, Android)

Native clients hit the same HTTP + WebSocket endpoints as browsers, with two notes:
  • CORS doesn’t apply — native HTTP clients skip CORS entirely. Don’t worry about PYLON_CORS_ORIGIN for them.
  • TLS is mandatory on iOS — App Transport Security rejects ws:// and http:// to non-localhost hosts. Always use wss:// and https:// in production. Self-signed certs work in dev with an ATS exception in Info.plist.
  • Background sessions — for large file uploads/downloads that should continue when the app is backgrounded, configure URLSessionConfiguration.background(...) in your transport. See Swift SDK.

Scale-out

Single-process by design. For higher throughput:
  • Reads: cache + rely on the 4-connection read pool (already in)
  • Writes: move to Postgres (postgres-live feature)
  • WS fanout: workers + Durable Objects; shape 3 amortizes edge
  • Shards: run one process per game region; load-balance by match id
Horizontal HA isn’t a first-class shape yet. If you need multi-master SQLite, you don’t want SQLite — switch to Postgres. For multi-region, on Pylon Cloud, set the workspace region during signup. Self-hosted multi-region is a manual exercise (one Pylon per region, app-level routing).

What about Docker?

The runtime image bundles the pylon binary plus Bun (which runs your TypeScript functions and SSR). It expects your app source mounted (or baked) at /app and keeps data on the /data volume:
Same env vars apply. Mount the data volume so it survives container restarts. For a self-contained image (no runtime mount) or a docker-compose setup with a Postgres sidecar, run pylon deploy --target docker or pylon deploy --target compose in your project — both generate a Dockerfile that copies your app onto the runtime image and serves it with pylon start.