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 and the experimental Workers path. To skip self-hosting, 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 them 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 must forward all relevant ports. For deployments with a single public port (Fly.io, Cloud Run), path-route the WebSocket, SSE, and shard upgrades through that one port at the proxy (see the Caddy and nginx examples below), then point the client’s sync wsUrl at the public origin. There is no server-side WS-URL environment variable; the client’s transport config tells it where to connect.

Shape 1: single VPS (SSH + systemd)

This is the simplest and cheapest shape: the pylon binary plus Bun, one systemd unit, and one reverse proxy. pylon deploy --target systemd generates this unit, plus step-by-step install commands, for your app. The shape looks like this:
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).
For backups, run pylon backup /var/backups/pylon/$(date +%F) nightly with cron. Test the restore quarterly, using 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. Compile Pylon with --features postgres-live and set 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. It uses the same components (Aurora, Fargate, ALB, EFS, CloudFront) and one CLI for deploys, secrets, and 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. Idle apps cost $0, because Workers scales to zero. Cost rises with request volume. See Workers costs. Workers does not yet support realtime shards (tick-based sims). They need persistent state that Workers alone cannot hold efficiently. Use shape 1 or 2 for game shards.

Shape 5: Pylon Cloud

Done. See Cloud for full details.

Shape 6: local dev

This starts on port 4321 with PYLON_DEV_MODE=true defaults: Studio at /studio, hot-reload, and permissive CORS. Do not use this mode in 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 so it drains unhealthy instances.

Shutdown and rolling deploys

SIGTERM terminates the process. The runtime does not yet install a signal handler, so SIGTERM does not run an in-process drain. The drain path exists in the runtime, but it is not wired to a signal yet. Two things make an abrupt stop safe in practice:
  • SQLite (WAL mode) is crash-consistent. An abrupt stop does not corrupt the database. The next start recovers the WAL, so there is no unflushed data to lose.
  • Drain at the load balancer, not in the process. For a rolling deploy 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.
For a rolling deploy: 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 send SIGTERM. PYLON_DRAIN_SECS (default 10s) bounds the drain loop for paths that do invoke the runtime’s shutdown, such as a programmatic shutdown. It is not a SIGTERM grace period.

Native clients (iOS, macOS, Android)

Native clients hit the same HTTP and WebSocket endpoints as browsers. Keep these points in mind:
  • CORS does not apply. Native HTTP clients skip CORS entirely. PYLON_CORS_ORIGIN does not affect them.
  • TLS is required on iOS. App Transport Security rejects ws:// and http:// to non-localhost hosts. Always use wss:// and https:// in production. Self-signed certificates work in dev with an ATS exception in Info.plist.
  • Background sessions. For large file uploads or downloads that should continue when the app is backgrounded, configure URLSessionConfiguration.background(...) in your transport. See Swift SDK.

Scale-out

Pylon supports multiple processes behind a load balancer. Use Postgres for shared entities, auth state, jobs, workflows, and sync state. Configure PYLON_CLUSTER_BUS for live change, presence, and CRDT fanout between self-hosted processes. Pylon Cloud configures its managed relay for you. Jobs and workflow steps use at-least-once execution. Make their handlers idempotent. SQLite remains a single-machine backend. See Horizontal scaling for the full setup and failure model. For multi-region on Pylon Cloud, set the workspace region during signup. Self-hosted multi-region is a manual setup: one Pylon process per region, with 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 commands generate a Dockerfile that copies your app onto the runtime image and serves it with pylon start.