---
title: 'Deploy Bun + Hono on Fly.io: 2026 Production Guide'
description: 'Deploy a production Bun + Hono app on Fly.io: multi-stage Bun Dockerfile, fly.toml with auto-stop machines and health checks, secrets, and graceful shutdown.'
url: https://nerdleveltech.com/deploy-bun-hono-fly-io-production-tutorial
language: en
date: '2026-05-21'
category: cloud-devops
tags:
  - bun
  - hono
  - fly.io
  - deployment
  - docker
  - edge
  - typescript
  - devops
  - cloud
markdown_url: https://nerdleveltech.com/deploy-bun-hono-fly-io-production-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/deploy-bun-hono-fly-io-production-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# Deploy Bun + Hono on Fly.io: 2026 Production Guide

Deploying a Bun + Hono app to Fly.io takes three things: a multi-stage Dockerfile, a `fly.toml` that defines one auto-stopping Machine, and `fly deploy`. This guide builds a production-grade Hono API and runs Hono on Fly.io as a single Machine that scales to zero between requests.

## TL;DR

You will build a small but production-shaped Hono API on the Bun runtime, containerize it with a multi-stage Dockerfile that runs as a non-root user, and deploy it to one Fly.io Machine. The `fly.toml` is tuned for the real world: request-based concurrency limits, an HTTP health check, a graceful-shutdown signal, and `auto_stop_machines = "stop"` so the Machine costs nothing while idle. Tools: Bun 1.3.14, Hono 4.12.21, Docker, and `flyctl`. Budget about 25 minutes start to finish, and by the end you will have Hono on Fly.io serving live HTTPS traffic.

## What you'll learn

- Scaffold a Bun + Hono project with a clean, testable file layout
- Build a Hono API with logging, request IDs, security headers, and a health endpoint
- Add an explicit `Bun.serve` entrypoint with graceful shutdown on SIGTERM
- Write a multi-stage Bun Dockerfile that runs as a non-root user
- Test the container locally before touching Fly.io
- Create a Fly.io app with `fly launch` and write a production `fly.toml`
- Tune auto-stop machines, concurrency limits, and health checks so the app scales to zero
- Store secrets with `fly secrets` and ship the app with `fly deploy`

## Prerequisites

You need four things installed locally:

- **Bun 1.3.14** — install with `curl -fsSL https://bun.com/install | bash`, or `brew install oven-sh/bun/bun`. Check with `bun --version`.[^bun]
- **Docker** — any recent Docker Desktop or Docker Engine. BuildKit is the default builder, so no extra flags are needed. You only need it for the local container test in Step 5.
- **flyctl** — the Fly.io CLI. Install it from the [official instructions](https://fly.io/docs/flyctl/install/) and run `fly auth login`. This guide was verified against flyctl v0.4.49.
- **A Fly.io account with a payment method.** Fly.io no longer has a permanent free tier — every organization needs a card on file, though new accounts get trial credit.[^pricing] The Machine in this guide is a `shared-cpu-1x` with 256 MB of RAM, which costs about **$1.94/month** if it runs continuously — and far less here, because it stops when idle.[^pricing]

A working knowledge of TypeScript and HTTP helps, but you do not need prior Bun, Hono, or Fly.io experience.

## Step 1 — Scaffold the Bun + Hono project

Create an empty directory and initialize a blank TypeScript project. The `-y` flag accepts every default, so `bun init` runs without prompts.[^buninit]

```bash
mkdir fly-hono-edge && cd fly-hono-edge
bun init -y
```

`bun init` writes `package.json`, `tsconfig.json`, a root `index.ts`, and `.gitignore`. Add Hono — `bun add` installs the package and writes the text lockfile `bun.lock`, which is the reproducibility anchor for the Docker build later:

```bash
bun add hono
```

We will keep server code under `src/`, so remove the placeholder entrypoint and create the source directory:

```bash
rm index.ts
mkdir src
```

Open `package.json` and replace the `scripts` block. `bun init` adds `@types/bun` to `devDependencies` — pin it to an explicit version so installs stay reproducible:

```json
{
  "name": "fly-hono-edge",
  "module": "src/index.ts",
  "type": "module",
  "private": true,
  "scripts": {
    "dev": "bun run --hot src/index.ts",
    "start": "bun run src/index.ts",
    "test": "bun test"
  },
  "dependencies": {
    "hono": "^4.12.21"
  },
  "devDependencies": {
    "@types/bun": "^1.3.14"
  }
}
```

The `bun.lock` file — not the caret ranges — is what guarantees a byte-identical dependency tree, because the Docker build installs with `--frozen-lockfile`. Commit `bun.lock` to version control.

## Step 2 — Build the Hono API: routes, middleware, and a health endpoint

Hono is a small web framework built on Web Standards (`Request`/`Response`), and it runs natively on Bun.[^hono] We will keep the Hono app in its own module so it can be imported by both the server entrypoint and the test file.

Create `src/app.ts`:

```ts
import { Hono } from 'hono'
import type { RequestIdVariables } from 'hono/request-id'
import { logger } from 'hono/logger'
import { requestId } from 'hono/request-id'
import { secureHeaders } from 'hono/secure-headers'
import { bearerAuth } from 'hono/bearer-auth'
import { HTTPException } from 'hono/http-exception'

const startedAt = Date.now()
const region = process.env.FLY_REGION ?? 'local'
const apiToken = process.env.API_TOKEN

// Typing the app with RequestIdVariables makes c.get('requestId') type-safe.
export const app = new Hono<{ Variables: RequestIdVariables }>()

// --- Global middleware ---
app.use('*', logger())
app.use('*', requestId())
app.use('*', secureHeaders())

// --- Public routes ---
app.get('/', (c) =>
  c.json({
    name: 'fly-hono-edge',
    message: 'Bun + Hono running on Fly.io',
    region,
    requestId: c.get('requestId'),
  }),
)

// Health endpoint — wired to the fly.toml health check in Step 7.
app.get('/health', (c) =>
  c.json({
    status: 'ok',
    region,
    uptimeSeconds: Math.round((Date.now() - startedAt) / 1000),
  }),
)

app.get('/api/hello/:name', (c) => {
  const name = c.req.param('name')
  return c.json({ greeting: `Hello, ${name}!`, region })
})

// --- Protected route — the token is supplied via `fly secrets` ---
const admin = new Hono<{ Variables: RequestIdVariables }>()
admin.use('*', (c, next) => {
  if (!apiToken) {
    return c.json({ error: 'API_TOKEN secret is not configured' }, 503)
  }
  return bearerAuth({ token: apiToken })(c, next)
})
admin.get('/stats', (c) =>
  c.json({
    bunVersion: Bun.version,
    pid: process.pid,
    rssMB: Math.round(process.memoryUsage().rss / 1024 / 1024),
    region,
  }),
)
app.route('/api/admin', admin)

// --- Error handling ---
app.notFound((c) => c.json({ error: 'Not Found', path: c.req.path }, 404))

app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return err.getResponse()
  }
  console.error(`[${c.get('requestId')}]`, err)
  return c.json({ error: 'Internal Server Error' }, 500)
})
```

A few production details are worth calling out. The `requestId` middleware tags every request with a UUID (it reuses an inbound `X-Request-Id` header if the client sends one) and exposes it through `c.get('requestId')`.[^requestid] The `logger` middleware writes one line per request to stdout — on Fly.io that stream becomes `fly logs`, so you get request logs for free. `secureHeaders` adds a sane set of response headers such as `X-Frame-Options` and `Strict-Transport-Security`.

The `/health` route is deliberately cheap: it returns HTTP 200 with no I/O, which is exactly what a load-balancer health check wants. The protected `/api/admin/stats` route reads a bearer token from `process.env.API_TOKEN`. If that secret is missing the route returns `503` instead of silently running unauthenticated — a small but real safety property. `region` comes from `FLY_REGION`, a three-letter region code that Fly.io injects into every Machine's runtime environment.[^flyruntime]

Now add a test file so you can verify behaviour without booting a server. Hono apps expose an `app.request()` method that takes a path and returns a `Response`. Create `src/app.test.ts`:

```ts
import { describe, expect, it } from 'bun:test'
import { app } from './app'

describe('fly-hono-edge', () => {
  it('GET /health returns ok', async () => {
    const res = await app.request('/health')
    expect(res.status).toBe(200)
    expect(await res.json()).toMatchObject({ status: 'ok' })
  })

  it('GET /api/hello/:name echoes the name', async () => {
    const res = await app.request('/api/hello/ada')
    expect(res.status).toBe(200)
    expect(await res.json()).toMatchObject({ greeting: 'Hello, ada!' })
  })

  it('protected route is 503 when API_TOKEN is unset', async () => {
    const res = await app.request('/api/admin/stats')
    expect(res.status).toBe(503)
  })
})
```

Run it with Bun's built-in test runner — no extra dependency needed:

```bash
bun test
```

```
 3 pass
 0 fail
```

## Step 3 — Add an explicit Bun.serve entrypoint with graceful shutdown

Bun will auto-serve a module that has a `default` export with a `fetch` handler, but that shortcut gives you no handle on the running server. For production you want the server object so you can shut it down cleanly. Create `src/index.ts`:

```ts
import { app } from './app'

const port = Number(process.env.PORT ?? 8080)

const server = Bun.serve({
  port,
  fetch: app.fetch,
  idleTimeout: 30,
})

console.log(`fly-hono-edge listening on ${server.url}`)

let shuttingDown = false

async function shutdown(signal: string): Promise<void> {
  if (shuttingDown) return
  shuttingDown = true
  console.log(`Received ${signal} - draining in-flight requests...`)
  await server.stop() // resolves once in-flight requests finish
  console.log('Server stopped cleanly')
  process.exit(0)
}

process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('SIGINT', () => shutdown('SIGINT'))
```

`Bun.serve` binds to `0.0.0.0` by default, which is required on Fly.io — the platform proxy reaches your app over a private network, so binding to `127.0.0.1` would make the Machine unreachable.[^bunserve] The `port` defaults to the `PORT` environment variable, but passing it explicitly keeps the intent obvious. `idleTimeout` is in seconds (default 10, max 255); 30 seconds is comfortable for a JSON API.[^bunserve]

The shutdown logic is the part most tutorials skip. `server.stop()` without arguments stops accepting new connections and resolves once in-flight requests finish; passing `true` would terminate connections immediately.[^bunserve] Registering handlers for both `SIGTERM` and `SIGINT` means the app drains cleanly whether it is stopped by Fly.io, by Docker, or by Ctrl+C during local development.

Start the server locally to confirm it boots:

```bash
bun run dev
```

```
fly-hono-edge listening on http://localhost:8080/
```

In another terminal, `curl http://localhost:8080/health` returns JSON like `{"status":"ok","region":"local","uptimeSeconds":3}`. Press Ctrl+C to stop the server — you will watch the full graceful-shutdown sequence run inside the container in Step 5.

## Step 4 — Write a multi-stage Bun Dockerfile

Fly.io deploys OCI container images, so the app needs a Dockerfile. A multi-stage build keeps the final image small by separating dependency installation from the runtime layer.[^bundocker] Create `Dockerfile`:

```dockerfile
# syntax=docker/dockerfile:1

# ---------- base ----------
FROM oven/bun:1.3.14-slim AS base
WORKDIR /app

# ---------- dependencies (production only) ----------
FROM base AS deps
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production

# ---------- runtime ----------
FROM base AS release
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY package.json bun.lock ./
COPY src ./src
USER bun
EXPOSE 8080
ENTRYPOINT ["bun", "run", "src/index.ts"]
```

The base image is `oven/bun:1.3.14-slim` — the Debian-slim variant of the official Bun image. Bun publishes `debian`, `slim`, `alpine`, and `distroless` variants for every release; `slim` is the pragmatic default because it keeps a shell for debugging while staying small. You can confirm the exact tag at [hub.docker.com/r/oven/bun/tags](https://hub.docker.com/r/oven/bun/tags).

`bun install --frozen-lockfile --production` installs exactly what `bun.lock` records and skips `devDependencies` — so `@types/bun` is never shipped to production.[^bundocker] The `deps` stage is cached independently of your source code, so editing a route does not re-run `bun install`.

`USER bun` is important: the `oven/bun` images ship a non-root `bun` user, and running as it limits the blast radius if the process is ever compromised.[^bundocker] `EXPOSE 8080` is documentation that Fly.io reads during `fly launch` to set the internal port. The `ENTRYPOINT` runs the server file directly.

Add a `.dockerignore` so the build context stays lean and test files never reach the image:

```
node_modules
.git
.gitignore
Dockerfile
.dockerignore
fly.toml
README.md
**/*.test.ts
.env
.env.*
```

Keep `bun.lock` out of `.dockerignore` — the `deps` stage copies it, and the build fails without it.

## Step 5 — Test the container locally

Never let Fly.io be the first place a container runs. Build and run it locally first:

```bash
docker build -t fly-hono-edge .
docker run --rm -p 8080:8080 fly-hono-edge
```

You should see `fly-hono-edge listening on http://localhost:8080/`. Hit the public routes:

```bash
curl localhost:8080/
curl localhost:8080/api/hello/ada
curl localhost:8080/health
```

The protected route returns `503` because no `API_TOKEN` is set inside the container yet:

```bash
curl -s localhost:8080/api/admin/stats
# {"error":"API_TOKEN secret is not configured"}
```

That is the expected, safe behaviour — the route refuses to serve until the secret exists. Stop the container with Ctrl+C; the graceful-shutdown handler logs `Server stopped cleanly` before exiting. If the container behaves here, it will behave on Fly.io, because Fly.io runs the exact same image.

## Step 6 — Create the Fly.io app with fly launch

`fly launch` inspects the project, creates a Fly.io app, and writes a `fly.toml`. Because a `Dockerfile` already exists in the directory, `fly launch` uses it instead of generating one. Pass `--no-deploy` so you can review and edit `fly.toml` before the first deploy:

```bash
fly launch --no-deploy
```

`flyctl` will prompt for an app name and a primary region, and ask whether to tweak settings. Accept a name (it must be globally unique, so Fly.io may append a suffix) and pick a region close to your users — this guide uses `ord` (Chicago). Decline any offer to add a database; this app does not need one.

When the command finishes you have a `fly.toml`. It already contains an `[http_service]` block with `internal_port = 8080` — Fly.io read that from your Dockerfile's `EXPOSE` line. The next step replaces that file with a production-tuned version.

## Step 7 — Configure fly.toml for production: auto-stop, concurrency, and health checks

This is where a hobby deploy becomes a production deploy. Open `fly.toml` and make it match the following — keep the `app` value that `fly launch` generated for you:

```toml
app = "fly-hono-edge"
primary_region = "ord"

# Fly sends this signal on stop/deploy; the app drains on SIGTERM.
kill_signal = "SIGTERM"
# Seconds to wait for a graceful exit before a hard kill (default 5, max 300).
kill_timeout = 10

[build]
  # Optional: flyctl already looks for ./Dockerfile by default.
  dockerfile = "Dockerfile"

[env]
  NODE_ENV = "production"
  PORT = "8080"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true
  min_machines_running = 0

  [http_service.concurrency]
    type = "requests"
    soft_limit = 200
    hard_limit = 250

  [[http_service.checks]]
    grace_period = "3s"
    interval = "15s"
    method = "GET"
    timeout = "4s"
    path = "/health"

[[vm]]
  size = "shared-cpu-1x"
  memory = "256mb"
```

Each block earns its place:

**Scale to zero.** `auto_stop_machines = "stop"` tells the Fly Proxy to stop the Machine when the app is idle, and `auto_start_machines = true` starts it again on the next request. With `min_machines_running = 0`, the Machine genuinely scales to zero — a stopped Machine bills only for its root filesystem at $0.15 per GB per 30 days, not for CPU or RAM.[^autostop][^pricing] Note that `auto_stop_machines` takes the string values `"off"`, `"stop"`, or `"suspend"` — the old boolean `true` that you will find in older blog posts is no longer the documented form, and `"suspend"` resumes faster than `"stop"` at the cost of holding more state.[^flycfg]

**Concurrency.** `type = "requests"` is recommended for HTTP apps because the Fly Proxy can pool and reuse connections; the default `"connections"` counts raw TCP sockets instead.[^flycfg] `soft_limit` is the threshold at which the proxy starts steering traffic to other Machines (and the figure it uses to decide a Machine is idle); `hard_limit` is where it stops sending new traffic entirely.[^flycfg]

**Health checks.** The `[[http_service.checks]]` block makes the Fly Proxy `GET /health` every 15 seconds and expect a 2xx response.[^flycfg] `grace_period = "3s"` gives the Machine time to boot before the first check — Bun and Hono start in well under a second, so three seconds is generous. One caveat from the Fly.io docs: an HTTP check does not follow 301/302 redirects, so if your app force-redirected `/health` to HTTPS the check would fail. This app returns 200 directly, so there is nothing to worry about.[^flycfg]

**Graceful shutdown.** The top-level `kill_signal` defaults to `SIGINT`, which the Fly.io docs note triggers a hard shutdown on most apps; overriding it to `SIGTERM` gives the softer, less disruptive shutdown this app is built for.[^flycfg] That pairs with the `SIGTERM` handler from Step 3. `kill_timeout = 10` gives the app ten seconds to drain in-flight requests before Fly.io forces the Machine down.[^flycfg]

**Machine size.** The `[[vm]]` block pins a `shared-cpu-1x` Machine with 256 MB of RAM. Pinning it matters for predictable scale-from-zero behaviour — without a `[[vm]]` section, `fly deploy` will not enforce a size.[^flycfg]

## Step 8 — Store secrets with fly secrets

The `[env]` block in `fly.toml` is for non-sensitive values only — it is committed to git in plain text. The API token belongs in Fly.io's encrypted secret store instead. Set it with `fly secrets set`:

```bash
fly secrets set API_TOKEN="$(openssl rand -hex 24)"
```

Secrets are exposed to the app as environment variables, exactly like `[env]` values, but they are encrypted at rest and never printed back. `fly secrets list` shows secret *names* and digests, never their values. If the app were already running, setting a secret would trigger a rolling restart so the new value takes effect; because nothing is deployed yet, Fly.io simply stages it for the first deploy.[^flysecrets]

If you want to verify the protected route after deploy, keep a copy of the generated token — `openssl rand -hex 24` prints it once. You can always rotate it later by running `fly secrets set` again.

## Step 9 — Deploy with fly deploy

Everything is in place. Ship it:

```bash
fly deploy
```

`fly deploy` builds the Docker image (using a remote builder by default), pushes it, and rolls the Fly Machine onto the new release.[^flylaunch] The first build downloads the `oven/bun` base image, so expect a minute or two; later deploys reuse cached layers and are much faster.

When it finishes, `flyctl` prints the app URL. Open it in a browser:

```bash
fly apps open
```

## Verification

Confirm the deployment from the command line. Replace `fly-hono-edge` with your actual app name:

```bash
curl https://fly-hono-edge.fly.dev/health
```

```json
{"status":"ok","region":"ord","uptimeSeconds":2}
```

A populated `region` field that matches your `primary_region` proves the request was served by the Fly Machine, not by something local. Check the other routes:

```bash
curl https://fly-hono-edge.fly.dev/
curl https://fly-hono-edge.fly.dev/api/hello/ada
```

Now exercise the secret. Without a token the protected route returns `401`; with the token you saved in Step 8 it returns `200` and the stats payload:

```bash
# 401 Unauthorized - no credentials
curl -s -o /dev/null -w '%{http_code}\n' https://fly-hono-edge.fly.dev/api/admin/stats

# 200 OK - authorized
curl -H "Authorization: Bearer YOUR_TOKEN_HERE" \
  https://fly-hono-edge.fly.dev/api/admin/stats
```

Inspect the Machine and stream its logs:

```bash
fly status
fly logs
```

In `fly logs` you will see one line per request from the Hono `logger` middleware. Leave the app untouched for a few minutes and `fly status` will show the Machine in a `stopped` state — that is `auto_stop_machines` doing its job. The next `curl` wakes it again, and `fly logs` will show the cold start followed by your request. The app is now live, costs nothing while idle, and drains cleanly on every deploy.

## Troubleshooting

**The deploy succeeds but the Machine never becomes healthy.** Almost always the app is not listening where Fly.io expects. Confirm `internal_port` in `fly.toml`, the `PORT` env var, and the port your code passes to `Bun.serve` are all the same value (8080 here). Also confirm the app binds `0.0.0.0` — `Bun.serve` does by default, so do not override `hostname` to `localhost`.

**Health checks keep failing.** Run `curl https://APP.fly.dev/health` directly. If it returns anything other than a 2xx status the check will fail. A too-short `grace_period` is the other common cause: if the check fires before the app finishes booting it records a failure. Three seconds is plenty for Bun + Hono, but a heavier app may need more.

**`bun install --frozen-lockfile` fails inside the Docker build.** This means `bun.lock` is out of sync with `package.json`, or it is missing from the build context. Run `bun install` locally to regenerate the lockfile, commit it, and make sure `bun.lock` is not listed in `.dockerignore`.

**The Machine never stops, or never starts.** If it never stops, check that `auto_stop_machines` is the string `"stop"` — not the deprecated boolean `true` from older guides — and that `min_machines_running` is `0`. If it never starts, make sure `auto_start_machines = true`. The Fly.io docs recommend keeping autostop and autostart either both enabled or both disabled.[^autostop]

**The first request after idle is slow.** That is the expected cost of scaling to zero — Fly.io must start the stopped Machine before serving. For a faster wake, switch `auto_stop_machines` to `"suspend"`, which resumes from a saved state. If you cannot tolerate any cold start, set `min_machines_running = 1`; that keeps one Machine running and costs roughly $1.94/month.[^pricing]

## Next steps and further reading

You now have a repeatable pattern for running Hono on Fly.io: a containerized Bun + Hono app, a production `fly.toml`, and a one-command deploy. From here you can scale to multiple regions by adding Machines with `fly scale count`, attach a managed Postgres or Upstash Redis from the Fly.io extensions catalog, or shrink the image further. Our walkthrough of [distroless container images for production builds](https://nerdleveltech.com/distroless-python-containers-with-uv-tutorial) shows the same multi-stage discipline taken one step further — `oven/bun:1.3.14-distroless` drops the shell entirely for a smaller attack surface.

For observability, wire structured request logs into a backend rather than reading `fly logs` by hand — the techniques in our guide to [edge observability with structured logs and Sentry](https://nerdleveltech.com/cloudflare-workers-observability-workers-logs-sentry-tutorial) port cleanly to a Fly.io app. And if the next thing you build is an API that other services authenticate against, our [production TypeScript server with OAuth and Streamable HTTP](https://nerdleveltech.com/mcp-server-typescript-oauth-streamable-http-production-tutorial) tutorial is a natural follow-on from the bearer-token pattern shown here.

The authoritative references for everything in this guide are the [Fly.io app configuration reference](https://fly.io/docs/reference/configuration/), the [Bun HTTP server docs](https://bun.com/docs/runtime/http/server), and the [Hono documentation](https://hono.dev/docs/).

[^bun]: Bun installation and current version. Bun 1.3.14 is the latest stable release (npm `bun` dist-tag `latest`, published 2026-05-13). https://bun.com/docs/installation
[^hono]: Hono on Bun — getting started guide. Hono 4.12.21 is the current release. https://hono.dev/docs/getting-started/bun
[^requestid]: Hono Request ID Middleware — generates a per-request UUID via `crypto.randomUUID()`, reuses an inbound `X-Request-Id` header, and exposes the value through `c.get('requestId')`. https://hono.dev/docs/middleware/builtin/request-id
[^buninit]: `bun init` reference — `-y` accepts defaults and scaffolds a blank TypeScript project non-interactively. https://bun.com/docs/runtime/templating/init
[^bunserve]: Bun HTTP server (`Bun.serve`) — `hostname` defaults to `0.0.0.0`, `port` defaults to the `PORT` env var, `idleTimeout` is in seconds (default 10, max 255), and `server.stop()` drains in-flight requests while `server.stop(true)` force-closes connections. https://bun.com/docs/runtime/http/server
[^bundocker]: Containerize a Bun application with Docker — multi-stage build, `bun install --frozen-lockfile --production`, and running as the non-root `bun` user. https://bun.com/guides/ecosystem/docker
[^flycfg]: Fly.io App configuration (`fly.toml`) reference — `[http_service]`, `[http_service.concurrency]`, `[[http_service.checks]]`, `[[vm]]`, `kill_signal`, and `kill_timeout`. https://fly.io/docs/reference/configuration/
[^flyruntime]: Fly.io — The Machine Runtime Environment. The three-letter region code is stored in the `FLY_REGION` environment variable on every started Machine. https://fly.io/docs/machines/runtime-environment/
[^autostop]: Fly.io — Automatically stop and start Machines. `auto_stop_machines` accepts `"off"`, `"stop"`, or `"suspend"`. https://fly.io/docs/launch/autostop-autostart/
[^pricing]: Fly.io Resource Pricing — a running `shared-cpu-1x` 256 MB Machine is $0.0027/hour (~$1.94/month); stopped Machines bill only for root filesystem storage at $0.15/GB per 30 days; all organizations require a payment method. https://fly.io/docs/about/pricing/
[^flysecrets]: Fly.io — Secrets. Encrypted environment variables; setting a secret on a running app triggers a restart. https://fly.io/docs/apps/secrets/
[^flylaunch]: Fly.io — Deploy an app and `fly launch`. `fly launch --no-deploy` creates the app and `fly.toml` without deploying; `fly deploy` builds and releases. https://fly.io/docs/launch/deploy/
