Deploy to Fly.io
Run the blog API on Fly.io from the Dockerfile of your project, with SQLite on a volume or with Postgres.
Note
These steps follow the Fly.io docs as of 2026-10-22 (checked on 2026-10-22). They were not run on Fly.io in CI. The Docker image itself was built and run locally.
What you need
- A Fly.io account and the
flyCLI (fly auth login). - The blog API from the tutorial, or any Dox app, with the
Dockerfileof a new Dox app. See Deployment: Docker for how that file works.
The image is small. For the blog API it is about 17 MB. Fly builds it for you on fly deploy.
Check for (native)
The project's Dockerfile copies libgcc_s.so.1 into the image, so the
native HTTP server loads. The start log ends with (native). If it says
using the dart:io server, see Docker.
1. Make the app on Fly.io
In the project folder:
fly launch finds the Dockerfile, asks for an app name and a region, and writes fly.toml.
--no-deploy stops before the first deploy, so you can set the config first.
2. Write fly.toml
Replace the file with this one. Change app and primary_region to your values.
app = "blog-api"
primary_region = "ams"
# Dox waits up to 8 seconds for open requests on stop. Fly waits 5 by default.
kill_timeout = 10
[build]
dockerfile = "Dockerfile"
# config that is not secret. Secrets come from `fly secrets set`.
[env]
APP_ENV = "production"
APP_PORT = "3000"
DB_CONNECTION = "sqlite"
DB_NAME = "/data/blog.sqlite"
[http_service]
internal_port = 3000
force_https = true
# one machine, always on: the SQLite file is on its volume
auto_stop_machines = "off"
auto_start_machines = true
min_machines_running = 1
[[http_service.checks]]
grace_period = "10s"
interval = "30s"
method = "GET"
timeout = "5s"
path = "/api/ping"
# the volume with the SQLite file
[mounts]
source = "blog_data"
destination = "/data"
[[vm]]
memory = "256mb"
cpu_kind = "shared"
cpus = 1
internal_portis the port the app listens on. Dox reads it fromAPP_PORT(orPORTwhenAPP_PORTis not set). Keep the two the same.- Values in
[env]must be strings, so write"3000", not3000. [[vm]]wins overfly scale vm. Eachfly deploysets the machine to these values.kill_timeoutis how long Fly waits afterSIGINTbefore it kills the app. Keep it aboveshutdownTimeoutofAppConfig(8 seconds by default). See Graceful shutdown.
3. Set the secrets
A secret is an environment variable on every machine of the app. fly secrets list shows the names, not the values.
A secret with the same name as an [env] key wins.
4. Choose a database
SQLite on a volume
Make the volume that [mounts] asks for, in the same region:
- One volume is attached to one machine only. Fly.io does not copy the data between volumes.
- So with SQLite, run exactly one machine.
Postgres
Create a Fly.io Managed Postgres cluster with fly mpg create.
Attach it to the app:
fly mpg attach sets one secret, DATABASE_URL. Dox reads it. When DATABASE_URL is set, it wins over DB_CONNECTION and the other DB_* keys. The app and the migrations both use it. See DATABASE_URL.
Then delete DB_CONNECTION and DB_NAME from [env], and delete the [mounts] section.
If the database asks for TLS, add ?sslmode=require to the url with fly secrets set DATABASE_URL=....
5. Deploy
fly deploy builds the image from the Dockerfile, makes the machine and starts it.
The app runs the migrations at start.
Check that there is one machine:
If the count is more than 1 and you use SQLite, run fly scale count 1.
Check it
The first log lines after a start:
Migrating: 2026_10_22_205345_create_posts_table
Migrated: 2026_10_22_205345_create_posts_table
{"level":"INFO","name":"dox","message":"Server started at http://[::]:3000 with 1 isolate (native)", ...}
HTTPS and the client IP
Fly.io ends TLS and sends plain HTTP to port 3000. force_https = true sends HTTP visitors to HTTPS.
To make req.ip() and req.scheme() show the real client, set trustedProxies in AppConfig.
The app port is reachable only through the Fly.io proxy, so ['*'] is safe here:
See Deployment: HTTPS.
More CPU, more machines
Dox starts one isolate per CPU core it sees. On cpus = 1 that is one isolate.
| You want | Do this |
|---|---|
| more speed on one machine | raise cpus in [[vm]] (and memory), then fly deploy. Dox uses the new cores by itself |
| more machines | use Postgres, then fly scale count 2. With SQLite, stay at one machine |
| a fixed number of isolates | set APP_TOTAL_ISOLATE in [env] |
Each machine is its own process. WebSocket rooms and the memory cache are not shared between machines.
Update the app
- Change config in
[env]and runfly deployagain. fly secrets setrestarts the machines with the new value. With--stageit only saves the value, and the next deploy uses it.- The app runs new migrations at start.
- With one machine there is a short gap while it restarts.
See also
- Deployment for the bundle and the
Dockerfile - Environments for all env keys and
APP_KEY - Docker for a health check binary and Docker Compose
- Fly.io docs: fly.toml, secrets, volumes, Managed Postgres