Documentation

Fly.io

What you'll have at the end: a Vura app deployed to Fly.io with persistent machines — the recommended host for hot routes (WebSockets, in-memory state).

Supports all route kinds: serverless, hot (WebSocket), task.

This deploy flow was live-verified on real Fly infrastructure on 2026-06-11 (preflight for this guide).


Why Fly for hot routes

Fly's persistent machines never idle-stop, which means in-memory WebSocket state survives across requests. The emitted fly.toml sets auto_stop_machines = "off" and min_machines_running = 1 so Fly never tears down your process between connections.


Steps

1. Scaffold and build

npm create vura@latest my-app
cd my-app
npm install
npm run build

vura build emits dist/fly.toml and dist/Dockerfile when hot routes are present.

2. The fly.toml

dist/fly.toml is generated by vura build. Its contents:

# Generated by vura build — hand-edits will be overwritten on next build.
# Deploy with: fly deploy ./dist
# (flyctl picks up this fly.toml from ./dist and uses ./dist as build context)
app = "my-app"
kill_signal = "SIGTERM"
kill_timeout = "30s"

[build]
  dockerfile = "Dockerfile"

[http_service]
  internal_port = 3000
  force_https = true
  # auto_stop_machines is off because hot routes hold in-memory websocket state
  auto_stop_machines = "off"
  min_machines_running = 1

Replace my-app with your Fly app name (or let fly apps create do it).

3. Create a Fly app (first deploy only)

fly apps create my-app

Skip fly launch — this guide uses fly deploy ./dist directly with the emitted fly.toml.

4. Deploy

fly deploy ./dist

flyctl reads dist/fly.toml, uses dist/ as the Docker build context (the same Dockerfile as the Docker guide), builds the image on Fly's builders, and rolls out the machine.

Expected output:

==> Building image
--> Building image done
==> Pushing image to the registry
==> Creating release
--> release v1 created
==> Deploying release v1
1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing]
--> v1 deployed successfully

5. Verify

fly status
curl -fsS https://my-app.fly.dev/api/hello

Smoke test

# Static page
curl -fsS https://my-app.fly.dev/ | grep -q '<h1'

# API route
curl -fsS https://my-app.fly.dev/api/hello

# WebSocket
wscat -c wss://my-app.fly.dev/api/chat

Drain on deploy

kill_signal = "SIGTERM" and kill_timeout = "30s" are set in the emitted fly.toml. When Fly rolls a new machine, it sends SIGTERM to the old one. The Vura server handles SIGTERM by stopping new connections and waiting up to 30 seconds for in-flight requests and active WebSocket connections to close before exiting. Connections that remain open after 30 seconds are closed with code 1001 (Going Away).

There is no drainTimeoutMs config key — the 30-second value comes from kill_timeout in fly.toml. Adjust it there if your workload needs more time.


CI-tested: this guide is verified by the fly job in .github/workflows/selfhost.yml. The job extracts the fly.toml from this guide and validates it with flyctl config validate. It then builds the same Docker image (from the docker guide's Dockerfile) and boots it locally to confirm / and /api/hello serve. It does not deploy to Fly — no cloud credentials are available in CI; Fly networking and anycast behavior are out of scope.


Route kind support

All kinds: serverless, hot (WebSocket), task / cron. Fly is the recommended host when your project has hot routes.