All deployment targets
Managed platform

Deploy to Fly.io

Runs the Docker image close to your users, with a real disk if you need one.

From
~$5
Ops effort
Low
Managed Postgres
Persistent disk
Best for

A single Go binary you want in several regions without running servers. The best fit for Grit’s single-binary mode.

Not for

Teams that want a click-through dashboard for everything — Fly is CLI-first and expects you to read a TOML file.

Setup

  1. 1

    Launch without deploying

    Let Fly detect the Dockerfile and write a fly.toml, but stop before it ships anything — the defaults need two changes first.

    fly launch --no-deploy
  2. 2

    Point the health check at the API

    Fly checks `/` by default. Grit serves its health endpoint at `/api/health`, so without this the machine is marked unhealthy and cycled forever while the app is running perfectly.

    [http_service]
    internal_port = 8080
    force_https = true
    auto_stop_machines = "suspend"
    auto_start_machines = true
    min_machines_running = 1
    [[http_service.checks]]
    interval = "15s"
    timeout = "3s"
    grace_period = "10s"
    method = "GET"
    path = "/api/health"
  3. 3

    Attach Postgres and Redis

    Attaching sets DATABASE_URL for you. Redis comes from Upstash through Fly and gives you REDIS_URL.

    fly postgres create --name my-app-db
    fly postgres attach my-app-db
    fly redis create
  4. 4

    Set the secrets

    Secrets are encrypted and injected at runtime. Setting them triggers a redeploy, so do it before the first one.

    fly secrets set \
    JWT_SECRET="$(openssl rand -base64 32)" \
    APP_ENV=production \
    CORS_ORIGINS="https://your-domain.com"
  5. 5

    Deploy, then migrate

    Grit does not auto-migrate on boot in production — a process that rewrites the schema every time it restarts is a bad idea when the platform can restart it for its own reasons. Run migrations as an explicit step.

    fly deploy
    fly ssh console -C "/app/migrate"

What catches people out

Machines suspend on idle by default. The first request after a quiet period pays the wake-up cost — set `min_machines_running = 1` for anything user-facing.

A volume is attached to one machine in one region. Scale past one machine and each gets its own empty disk, so uploads must go to S3/R2 rather than local storage.

Fly Postgres is an unmanaged Postgres you own, not a managed service. Backups are your job — `fly postgres` will not do point-in-time recovery for you.

Platform dashboards and CLI flags change faster than these docs. For anything that looks different from what is written here, Fly.io's own documentation is the authority: fly.io/docs/