Skip to content

Deploy on Fly.io

Fly runs your container as Machines close to your users, with private networking between apps. It builds straight from a Dockerfile in your repo, so the skein image needs no registry of its own.

Everything platform-agnostic — environment variables, pool sizing, probes, scaling caveats — is in deploy.md.

Before you start

Install flyctl and log in (fly auth login). Fly builds remotely by default, so you don't need Docker locally and the architecture is handled for you.

1. Commit the Dockerfile

bash
skein dockerfile -o Dockerfile

Commit it. Note the build context: the Dockerfile builds a .skein/build artifact, so run skein build in CI (or before fly deploy) to produce it — Fly deploys the directory containing the artifact.

2. Provision Postgres + Redis

bash
fly postgres create      # or use Fly Managed Postgres, or any external provider
fly redis create         # Upstash Redis, provisioned through Fly

Fly's unmanaged Postgres is a database you operate — fine for smaller deployments, but read Fly's docs on what they do and don't take responsibility for. Managed Postgres or an external provider (Neon, Supabase) is the lower-effort choice.

Attach them and set the two variables skein reads:

bash
fly secrets set \
  POSTGRES_URI="postgres://…" \
  REDIS_URI="redis://…"

Prefer the .internal (6PN private network) hostnames. Private traffic is plaintext inside your organization's network, so no TLS configuration is needed.

3. Configure and deploy

toml
# fly.toml
app = "my-skein-app"
primary_region = "iad"

[build]

[http_service]
  internal_port = 8123          # must match the port the app binds; see the note below
  force_https = true
  auto_stop_machines = "off"    # see the background-runs caveat below
  auto_start_machines = true
  min_machines_running = 1

  [[http_service.checks]]
    grace_period = "20s"        # boot runs migrations before listening
    interval = "30s"
    timeout = "5s"
    method = "GET"
    path = "/ok"

[env]
  PG_POOL_MAX = "5"
  SKEIN_RUN_CONCURRENCY = "5"

# Fly's default kill_timeout is 5s — too tight for skein's drain plus abort.
kill_signal = "SIGTERM"
kill_timeout = "15s"
bash
fly deploy

4. Verify

Run the verification sequence against https://my-skein-app.fly.dev.

Fly caveats

Fly does not set PORT — keep internal_port in sync yourself

Unlike Railway, Render, Cloud Run and App Runner, Fly injects no PORT variable; internal_port is only an instruction to Fly's proxy, and the app is expected to know its own port. The config above works because internal_port matches the port skein binds when nothing tells it otherwise (8123).

So if you change one, change the other. Either set internal_port to 8123 and leave it alone, or declare the port explicitly on both sides:

toml
[http_service]
  internal_port = 3000

[env]
  PORT = "3000"

A mismatch here produces Fly's app is not listening on the expected address warning and a machine that never passes its health check.

Background runs need a machine that stays awake

auto_stop_machines suspends or stops a Machine when it has no traffic. skein's background runs execute after the HTTP request returns, so a machine that stops the moment the response is sent will freeze them mid-run. Set auto_stop_machines = "off" and min_machines_running = 1.

If you only use inline runs (/runs/wait, /runs/stream), auto-stop is fine — the work happens during the request.

Raise kill_timeout

Fly's default is 5 seconds, which is exactly skein's drain window with nothing left for the abort step that settles in-flight runs terminally. 15s gives the default shutdown sequence room; raise both together if you increase SKEIN_SHUTDOWN_GRACE_MS.

Multi-region

Machines in several regions all talk to one Postgres primary, so far-away regions pay the write latency. Multi-region here buys you edge termination, not database HA. And with more than one Machine running, read Scaling past one instance.