Skip to main content
Pylon runs as a single binary 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

# Core
PYLON_PORT=4321
PYLON_DB_PATH=/var/lib/pylon/pylon.db
PYLON_FILES_DIR=/var/lib/pylon/uploads
PYLON_MANIFEST=/etc/pylon/pylon.manifest.json

# Auth (MUST be set in non-dev)
PYLON_ADMIN_TOKEN=<64+ random bytes, hex>
PYLON_SESSION_DB=/var/lib/pylon/sessions.db

# Client-facing
PYLON_CORS_ORIGIN=https://your-app.com
PYLON_CSRF_ORIGINS=https://your-app.com

# Mode switch
PYLON_DEV_MODE=false
Optional:
PYLON_JOBS_DB=/var/lib/pylon/jobs.db          # durable job queue
PYLON_OAUTH_GOOGLE_CLIENT_ID=...              # enable Google OAuth
PYLON_OAUTH_GITHUB_CLIENT_ID=...              # enable GitHub OAuth
PYLON_EMAIL_PROVIDER=stack0                   # or sendgrid | resend | webhook
PYLON_EMAIL_API_KEY=sk_live_...               # provider API key
PYLON_EMAIL_FROM=noreply@yourdomain.com       # verified sender
PYLON_EMAIL_HTTP_URL=...                      # only when PYLON_EMAIL_PROVIDER=webhook

# File storage (defaults to local disk)
PYLON_FILES_PROVIDER=stack0                   # or local (default)
PYLON_STACK0_API_KEY=sk_live_...              # required when provider=stack0
PYLON_STACK0_FOLDER=uploads                   # optional folder/prefix
PYLON_FILES_DIR=/var/lib/pylon/uploads        # local provider only
PYLON_FILES_URL_PREFIX=/api/files             # local provider only

# Cookie auth (browser apps)
PYLON_COOKIE_DOMAIN=.your-app.com
PYLON_COOKIE_SAMESITE=lax
PYLON_COOKIE_SECURE=true
Hard requirements that fail to start:
  • PYLON_CORS_ORIGIN=* in non-dev mode is refused
  • 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:
PortPurposeWhen
PYLON_PORT (default 4321)HTTPAlways
PYLON_PORT + 1WebSocketWhen sync engine uses transport: websocket (default)
PYLON_PORT + 2SSE (/events)When clients use transport: sse fallback
PYLON_PORT + 3Realtime shardsWhen apps use useShard / PylonRealtime
Your reverse proxy needs to forward all relevant ports. For deployments with a single public port (Fly.io, Cloud Run), set PYLON_WS_URL so clients know where to connect.

Shape 1: single VPS (SSH + systemd)

Simplest and cheapest. One binary, one systemd unit, one reverse proxy.
# /etc/systemd/system/pylon.service
[Unit]
Description=pylon
After=network-online.target

[Service]
EnvironmentFile=/etc/pylon/env
ExecStart=/usr/local/bin/pylon serve
Restart=on-failure
RestartSec=5s
User=pylon
Group=pylon
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/lib/pylon

[Install]
WantedBy=multi-user.target
Reverse proxy (Caddy or nginx) forwards :443:4321 plus WebSocket upgrades for /ws:4322, /events (SSE) → :4323, and shard WS → :4324. A sample nginx config ships in deploy/terraform/nginx.conf.
systemctl enable --now pylon
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

your-app.com {
    reverse_proxy localhost:4321

    handle /ws {
        reverse_proxy localhost:4322
    }
    handle /events {
        reverse_proxy localhost:4323
    }
    handle /shard/* {
        reverse_proxy localhost:4324
    }
}

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.
sst deploy --stage production
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

pylon login
pylon deploy --target cloud
Done. See Cloud for full details.

Shape 6: local dev

pylon 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.

Graceful shutdown

Send SIGTERM. The server:
  1. Stops accepting new connections
  2. Lets in-flight HTTP requests finish (30s cap)
  3. Closes WS + SSE with a normal close frame
  4. Flushes the WAL
  5. Exits with 0
Rolling deploys are safe — start the new instance, let the load balancer promote it, send SIGTERM to the old one.

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?

docker run -d \
  --name pylon \
  -p 4321:4321 -p 4322:4322 -p 4323:4323 -p 4324:4324 \
  -v /var/lib/pylon:/data \
  --env-file /etc/pylon/env \
  ghcr.io/pylonsync/pylon:latest
Same env vars apply. Mount the data volume so it survives container restarts. The image is ~30 MB (statically linked, alpine-based). For docker-compose with a Postgres sidecar, see deploy/docker-compose.yml.